Tools in Geppetto (Turn-based)

A complete guide to defining, attaching, and executing tools with Turns. Tool registries are carried via `context.Context` (Turn state stays serializable).

Sections

Terminology & Glossary
📖 Documentation
Navigation
58 sectionsv0.1
📄 Tools in Geppetto (Turn-based) — glaze help geppetto-tools
geppetto-tools

Tools in Geppetto (Turn-based)

A complete guide to defining, attaching, and executing tools with Turns. Tool registries are carried via `context.Context` (Turn state stays serializable).

Tutorialgeppettotoolsturnsmiddlewarehelpers

Tools in Geppetto (Turn-based)

Why Tools?

Large language models can generate text, but they can't access databases, call APIs, or perform calculations directly. Tools bridge this gap by letting models request specific function calls with structured inputs.

When a model needs information it doesn't have (like today's weather) or wants to perform an action (like sending an email), it emits a tool call with the function name and arguments. Your code executes the function and returns the result, allowing the model to continue with fresh information.

Example flow:

User: "What's the weather in Paris?"
  ↓
Model: tool_call {name: "get_weather", args: {location: "Paris"}}
  ↓
Your code: executes get_weather("Paris") → {temp: 18, conditions: "Cloudy"}
  ↓
Model: "The weather in Paris is 18°C and cloudy."

Architecture Overview

In the Turn-based architecture:

  • Provider engines emit tool_call blocks when models request tools
  • The tool loop runner executes tools and appends tool_use blocks
  • Engines re-run with the updated Turn to let the model continue

Key Pattern: The runtime tools.ToolRegistry is carried via context.Context (see tools.WithRegistry). Only serializable tool configuration lives on Turn.Data (e.g., engine.KeyToolConfig). This keeps Turn state persistable while allowing dynamic tools per inference call.

Packages

import (
    "github.com/go-go-golems/geppetto/pkg/inference/engine"
    "github.com/go-go-golems/geppetto/pkg/inference/session"
    "github.com/go-go-golems/geppetto/pkg/inference/toolloop"
    "github.com/go-go-golems/geppetto/pkg/inference/toolloop/enginebuilder"
    "github.com/go-go-golems/geppetto/pkg/inference/tools"
    "github.com/go-go-golems/geppetto/pkg/turns"
)

What you’ll learn

  • How to define tools and register them
  • How to attach tools to a Turn for provider advertisement
  • How the tool loop runner executes tools
  • How engines map model outputs to Turn blocks

Key concepts (at a glance)

  • Registry: tools.ToolRegistry holds callable tools
  • Per-Turn tools:
    • Runtime registry: carried via context.Context using tools.WithRegistry(ctx, reg)
    • Serializable config: stored on Turn.Data via engine.KeyToolConfig
  • Blocks: llm_text, tool_call, tool_use

OpenAI Responses specifics

When using the OpenAI Responses engine (ai-api-type=openai-responses):

  • Tools are advertised via the tools array on the request. For function tools, schema is top-level: {type: "function", name, description, parameters}.
  • The engine streams function_call arguments via SSE and emits a tool-call event when the function_call completes.
  • Reasoning summary is streamed as partial-thinking / EventThinkingPartial events. UIs can render it between "Thinking started/ended" markers and should use the event's Completion field for accumulated thinking text.
  • The next iteration (not yet implemented in docs) will include the assistant:function_call and tool:tool_result blocks in the next request’s input to continue tool-driven workflows.
  • Payload keys: use turns.PayloadKeyText, turns.PayloadKeyID, turns.PayloadKeyName, turns.PayloadKeyArgs, turns.PayloadKeyResult, turns.PayloadKeyError

Quickstart: From zero to tool execution

  1. Define a tool function and register it
type WeatherRequest struct {
    Location string `json:"location" jsonschema:"required"`
    Units    string `json:"units,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,default=celsius"`
}

type WeatherResponse struct {
    Location    string
    Temperature float64
}

func weatherTool(req WeatherRequest) WeatherResponse {
    return WeatherResponse{Location: req.Location, Temperature: 22}
}

reg := tools.NewInMemoryToolRegistry()
def, _ := tools.NewToolFromFunc("get_weather", "Get weather", weatherTool)
_ = reg.RegisterTool("get_weather", *def)
  1. Run the tool loop (the loop attaches the registry to context and manages tool config on the Turn)
seed := &turns.Turn{}
turns.AppendBlock(seed, turns.NewUserTextBlock("What's the weather in Paris? Use get_weather."))

loopCfg := toolloop.NewLoopConfig().
    WithMaxIterations(5)

toolCfg := tools.DefaultToolConfig().
    WithMaxParallelTools(1).
    WithToolChoice(tools.ToolChoiceAuto).
    WithToolErrorHandling(tools.ToolErrorContinue)

loop := toolloop.New(
    toolloop.WithEngine(e),
    toolloop.WithRegistry(reg),
    toolloop.WithLoopConfig(loopCfg),
    toolloop.WithToolConfig(toolCfg),
)
updated, err := loop.RunLoop(ctx, seed)
  1. Alternatively: run via the session builder
runner, _ := enginebuilder.New(
    enginebuilder.WithBase(e),
    enginebuilder.WithToolRegistry(reg),
    enginebuilder.WithLoopConfig(loopCfg),
    enginebuilder.WithToolConfig(toolCfg),
).Build(ctx, "demo-session")
updated, _ := runner.RunInference(ctx, seed)

How to wire tools end-to-end

This is the minimal “wire it up and run” pattern. It assumes you already have an engine.Engine (via factory.NewEngineFromParsedValues(...) or your own builder) and a populated tools.ToolRegistry:

import (
    "context"
    "time"

    "github.com/go-go-golems/geppetto/pkg/events"
    "github.com/go-go-golems/geppetto/pkg/inference/engine"
    "github.com/go-go-golems/geppetto/pkg/inference/toolloop"
    "github.com/go-go-golems/geppetto/pkg/inference/tools"
    "github.com/go-go-golems/geppetto/pkg/turns"
)

func RunWithTools(ctx context.Context, eng engine.Engine, reg tools.ToolRegistry, seed *turns.Turn, sinks ...events.EventSink) (*turns.Turn, error) {
    if len(sinks) > 0 {
        ctx = events.WithEventSinks(ctx, sinks...)
    }

    loopCfg := toolloop.NewLoopConfig().WithMaxIterations(5)
    toolCfg := tools.DefaultToolConfig().WithExecutionTimeout(60 * time.Second)

    loop := toolloop.New(
        toolloop.WithEngine(eng),
        toolloop.WithRegistry(reg),
        toolloop.WithLoopConfig(loopCfg),
        toolloop.WithToolConfig(toolCfg),
    )
    return loop.RunLoop(ctx, seed)
}

Guided walkthrough: End-to-end example

The following example shows how to:

  • Define a tool with JSON Schema inferred from a Go function
  • Seed a Turn with a per-Turn registry
  • Let the engine advertise tools to the provider
  • Execute tools via the tool loop and return results as tool_use blocks
package main

	import (
	    "context"
	    "github.com/go-go-golems/geppetto/pkg/inference/engine"
		    "github.com/go-go-golems/geppetto/pkg/inference/toolloop"
	    "github.com/go-go-golems/geppetto/pkg/inference/toolloop/enginebuilder"
	    "github.com/go-go-golems/geppetto/pkg/inference/tools"
	    "github.com/go-go-golems/geppetto/pkg/turns"
	)

type AddRequest struct { A, B float64 `json:"a" jsonschema:"required"` }
type AddResponse struct { Sum float64 `json:"sum"` }

func addTool(req AddRequest) AddResponse { return AddResponse{Sum: req.A + req.B} }

	func run(ctx context.Context, e engine.Engine) error {
	    // 1) Create registry and register the tool
	    reg := tools.NewInMemoryToolRegistry()
	    def, _ := tools.NewToolFromFunc("add", "Add two numbers", addTool)
	    _ = reg.RegisterTool("add", *def)

	    // 2) Seed a Turn
	    t := &turns.Turn{}
	    turns.AppendBlock(t, turns.NewUserTextBlock("Please use add with a=2 and b=3"))

	    // 3) Configure the tool loop
	    loopCfg := toolloop.NewLoopConfig().
	        WithMaxIterations(3)

	    toolCfg := tools.DefaultToolConfig().
	        WithMaxParallelTools(1).
	        WithToolChoice(tools.ToolChoiceAuto).
	        WithToolErrorHandling(tools.ToolErrorContinue)

	    // 4) Build a runner that owns tool execution
		    builder := enginebuilder.New(
		        enginebuilder.WithBase(e),
		        enginebuilder.WithToolRegistry(reg),
		        enginebuilder.WithLoopConfig(loopCfg),
		        enginebuilder.WithToolConfig(toolCfg),
		    )
		    runner, err := builder.Build(ctx, "demo-session")
	    if err != nil {
	        return err
	    }

	    // 5) Run inference (engine may emit tool_call; tool loop executes and appends tool_use)
	    _, err = runner.RunInference(ctx, t)
	    return err
	}

Tool executors and lifecycle hooks

Once a provider emits tool_call blocks, a tools.ToolExecutor turns those calls into actual function invocations. Geppetto now ships two composable executors:

  • tools.DefaultToolExecutor wraps the standard behavior (argument masking, event publishing, retries, and parallelism driven by ToolConfig)
  • tools.BaseToolExecutor provides the orchestration plus overridable lifecycle hooks so you can inject authorization, observability, or custom retry heuristics

The ToolConfig you attach to the Turn still governs concurrency (MaxParallelTools), error handling (ToolErrorAbort vs ToolErrorRetry), and retry backoff. DefaultToolExecutor simply wires those settings into the base implementation.

If you need custom behavior, embed the base executor and override only the hooks you care about. Remember to point the base executor back to the outer type so the overrides run.

import (
    "context"
    "encoding/json"

    "github.com/go-go-golems/geppetto/pkg/inference/tools"
)

type Session interface {
    Bearer() string
}

type AuthorizedExecutor struct {
    *tools.BaseToolExecutor
    sess Session
}

func NewAuthorizedExecutor(cfg tools.ToolConfig, sess Session) *AuthorizedExecutor {
    base := tools.NewBaseToolExecutor(cfg)
    exec := &AuthorizedExecutor{BaseToolExecutor: base, sess: sess}
    base.ToolExecutorExt = exec // enable hook overrides
    return exec
}

func (a *AuthorizedExecutor) PreExecute(ctx context.Context, call tools.ToolCall, _ tools.ToolRegistry) (tools.ToolCall, error) {
    // Inject auth into the argument payload before execution
    var args map[string]any
    _ = json.Unmarshal(call.Arguments, &args)
    if args == nil {
        args = map[string]any{}
    }
    args["auth"] = map[string]string{"bearer_token": a.sess.Bearer()}
    call.Arguments, _ = json.Marshal(args)
    return call, nil
}

func (a *AuthorizedExecutor) MaskArguments(ctx context.Context, call tools.ToolCall) string {
    // Redact secrets when events are published
    var args map[string]any
    _ = json.Unmarshal(call.Arguments, &args)
    if auth, ok := args["auth"].(map[string]any); ok {
        auth["bearer_token"] = "***"
    }
    masked, _ := json.Marshal(args)
    return string(masked)
}

Available hooks on BaseToolExecutor:

  • PreExecute mutate or reject calls before lookup
  • IsAllowed add executor-specific authorization checks before execution
  • MaskArguments, PublishStart, PublishResult tune event payloads
  • ShouldRetry implement bespoke retry policies
  • MaxParallel override concurrency control per batch

Override whichever hooks you need; the base executor handles the rest (context cancellation, event emission, timings, and retries). For most projects, tools.NewDefaultToolExecutor remains sufficient, and higher-level orchestration (via toolloop.Loop or toolloop/enginebuilder) wires it in under the hood.


Context-aware tool functions

tools.NewToolFromFunc recognises optional context.Context parameters. Supported signatures include:

  • func(Input) (Output, error)
  • func(context.Context, Input) (Output, error)
  • func(context.Context) (Output, error) (no JSON payload)
  • func() (Output, error)

When you register a tool, Geppetto generates JSON Schema for the first non-context parameter and compiles both context-free and context-aware executors. That means providers that pass Go contexts can propagate deadlines, auth tokens, or tracing spans straight into your tool implementation.

func searchDocs(ctx context.Context, req SearchRequest) (SearchResponse, error) {
	span := trace.SpanFromContext(ctx)
	span.AddEvent("tool.searchDocs")
	return index.Search(ctx, req.Query)
}

def, _ := tools.NewToolFromFunc("search_docs", "Search internal documentation", searchDocs)

If a tool has no JSON input (for example func(context.Context) (Result, error)), the generated schema becomes an empty object so the provider can still advertise the tool.


Reference: payload and data keys

When reading/writing block payloads, always use the constants:

turns.PayloadKeyText
turns.PayloadKeyID
turns.PayloadKeyName
turns.PayloadKeyArgs
turns.PayloadKeyResult
turns.PayloadKeyError

Engine discovery keys in Turn.Data:

engine.KeyToolConfig      // engine.ToolConfig stored in Turn.Data

Best practices

  • Define precise JSON Schemas; mark required params judiciously
  • Keep tool inputs small (provider payload limits apply)
  • Log tool execution steps and responses
  • Use timeouts and iteration limits to prevent loops
  • Prefer middleware for Turn-native automation; use helpers when you want explicit control over the loop

Emitting Custom Events from Tools

Tools can emit custom progress or status events using the event registry. This is useful for long-running operations where you want to provide real-time feedback to users:

import "github.com/go-go-golems/geppetto/pkg/events"

type ToolProgressEvent struct {
    events.EventImpl
    ToolName string  `json:"tool_name"`
    Progress float64 `json:"progress"`
    Message  string  `json:"message"`
}

func init() {
    _ = events.RegisterEventFactory("tool-progress", func() events.Event {
        return &ToolProgressEvent{EventImpl: events.EventImpl{Type_: "tool-progress"}}
    })
}

func longRunningTool(ctx context.Context, req ToolRequest) (ToolResponse, error) {
    // Emit progress events
    progressEvent := &ToolProgressEvent{
        EventImpl: events.EventImpl{Type_: "tool-progress", Metadata_: metadata},
        ToolName:  "long_running_tool",
        Progress:  0.5,
        Message:   "Processing data...",
    }
    events.PublishEventToContext(ctx, progressEvent)
    
    // ... tool implementation
}

For details on event extensibility, see: glaze help geppetto-events-streaming-watermill

Troubleshooting and Tips

ProblemSolution
Tools not advertisedEnsure ctx = tools.WithRegistry(ctx, reg) before RunInference
Tool call not executedCheck middleware is attached or run the tool loop explicitly via toolloop.New(...).RunLoop(...)
Payload key errorsUse constants like turns.PayloadKeyArgs, never string literals
Dynamic tools not workingModify registry before calling RunInference; Turn.Data for config only

See Also