gocode

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2026 License: MIT Imports: 16 Imported by: 0

README

gocode

A small Go library for LLM calls, tools, and agent loops.

Plain data. Plain functions. No framework magic.

You own the data. You own the tools. You own the loop.

gocode scales from one model call to practical tool-using assistants without forcing a framework-shaped runtime onto simple programs.

It gives you:

  • Ask and AskStream for model calls
  • Loop and LoopStream for tool-using loops
  • Extract[T] for typed structured output (with or without intermediate tool use)
  • plain []Message history and normal Go functions as tools
  • providers for Anthropic, OpenAI, and OpenRouter
  • typed tools, schema helpers, toolsets, middleware, context management, MCP, and a thin Agent block
  • safe built-in tools (clock, math, sandboxed workspace)
  • session persistence with a five-method Store interface
  • retries, typed errors, streaming, usage tracking

Requires Go 1.21+. No external dependencies in the core package.

Install

go get github.com/lukemuz/gocode

Set an API key for the provider you want:

export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
export OPENROUTER_API_KEY=sk-or-...

The smallest useful call

client, err := anthropic.NewClientFromEnv(gocode.ModelSonnet)
if err != nil {
    log.Fatal(err)
}

history := []gocode.Message{
    gocode.NewUserMessage("Give me three practical ideas for using LLMs in a Go service."),
}

reply, _, err := client.Ask(context.Background(), "You are concise.", history)
if err != nil {
    log.Fatal(err)
}

fmt.Println(gocode.TextContent(reply))

No hidden session. No runner. history is just data.

For a step-by-step walkthrough see QUICKSTART.md. For the design philosophy see VISION.md. For an honest comparison with Google's ADK see COMPARISON.md.

Core building blocks

Provider

A Provider translates between gocode's data model and an LLM API.

type Provider interface {
    Call(ctx context.Context, req ProviderRequest) (ProviderResponse, error)
    Stream(ctx context.Context, req ProviderRequest, onDelta func(ContentBlock)) (ProviderResponse, error)
}

Anthropic, OpenAI Chat Completions, OpenAI Responses, and OpenRouter are included. Any backend can implement the interface.

Client

A Client holds provider, model, token limit, and retry config. It does not store conversation state, so the same client reuses across conversations, requests, jobs, and goroutines.

client, err := gocode.New(gocode.Config{
    Provider:  provider,
    Model:     gocode.ModelSonnet,
    MaxTokens: 4096,
})
Message

Conversation history is plain data. Append replies yourself when you want to continue:

history := []gocode.Message{gocode.NewUserMessage("Hello")}

reply, _, err := client.Ask(ctx, system, history)
history = append(history, reply, gocode.NewUserMessage("Tell me more."))
Tool and ToolFunc

A tool has two parts: a model-facing definition and a Go function.

tool, fn := gocode.NewTypedTool(
    "calculator",
    "Do basic arithmetic.",
    gocode.Object(
        gocode.String("operation", "add, subtract, multiply, or divide", gocode.Required()),
        gocode.Number("a", "First number", gocode.Required()),
        gocode.Number("b", "Second number", gocode.Required()),
    ),
    func(ctx context.Context, in CalculatorInput) (string, error) {
        return calculate(in)
    },
)

Tools compile down to ordinary values. A Toolset is an ordered slice of ToolBinding{Tool, Func, Meta}. gocode.Tools(...) and gocode.Bind(tool, fn) are variadic constructors for the common case:

tools := gocode.Tools(
    gocode.Bind(tool, fn),
    gocode.Bind(other, otherFn),
)

No hidden registry.

From one call to a tool loop

One model call
reply, usage, err := client.Ask(ctx, system, history)

usage reports input/output tokens so cost-conscious code doesn't have to drop down to Loop.

Tool loop
result, err := client.Loop(ctx, system, history, tools, 5)
history = result.Messages
fmt.Println(result.FinalText())

Loop calls the model, runs requested tools, appends tool results, and repeats until the model returns a final answer or the iteration limit. Multiple tool calls in one model turn run concurrently and return in original order.

Because Ask, Loop, and Agent.Step are ordinary calls over plain data, they compose like any Go function — run two tool-using loops in parallel with gocode.Parallel, then synthesize their outputs with a later Ask.

Typed extraction

When you want a typed Go value back — with or without intermediate tool use — Extract runs a loop in which the model must call a single "submit" tool whose typed argument is the return value:

type Plan struct {
    Steps []string `json:"steps"`
}

plan, result, err := gocode.Extract[Plan](ctx, client, system, history,
    gocode.ExtractParams[Plan]{
        Description: "Submit the final plan as a list of ordered steps.",
        Schema: gocode.Object(
            gocode.Array("steps", "ordered steps",
                gocode.SchemaProperty{Type: "string"}, gocode.Required()),
        ),
        // Tools: searchTools,           // optional: search-then-submit
        // Validate: func(p Plan) error  // optional: reject and let the model retry
    })

Extract is built on ToolMetadata.Terminal — a flag that tells Loop to short-circuit when a tool is invoked successfully. You can set it yourself for hand-rolled submit patterns; Extract is the headline sugar.

Practical assembly

Toolsets and middleware
toolset := gocode.MustJoin(clockTool.Toolset(), workspaceToolset).Wrap(
    gocode.WithTimeout(5*time.Second),
    gocode.WithResultLimit(20_000),
    gocode.WithConfirmation(confirm),
)

result, err := client.Loop(ctx, system, history, toolset, 10)

MustJoin is for static composition where a duplicate tool name is a programmer error. Join returns an error for dynamic composition.

Available middleware: WithTimeout, WithResultLimit, WithLogging, WithPanicRecovery, WithConfirmation. Metadata is advisory; your application decides policy.

Context management

ContextManager trims history explicitly before a call.

cm := gocode.ContextManager{MaxTokens: 8000, KeepFirst: 1, KeepRecent: 20}
trimmed, err := cm.Trim(ctx, history)

The original history is not mutated. Tool-use/tool-result integrity is preserved. Summarization happens only if you configure a summarizer.

Agent

Agent is the blessed middle path: a thin block over a client, prompt, toolset, context manager, iteration limit, and hooks.

a := gocode.Agent{
    Client:  client,
    System:  "You are a helpful assistant.",
    Tools:   toolset,
    Context: gocode.ContextManager{MaxTokens: 8000, KeepRecent: 20},
    MaxIter: 10,
}

// One-shot autonomous task: pass the goal as a single user message.
result, err := a.Step(ctx, []gocode.Message{gocode.NewUserMessage("do the thing")})

// Multi-turn: call Step once per human turn, threading history.
result, err = a.Step(ctx, history)

Step trims history once up front and again before every model call inside the loop (when a ContextManager is configured), so long autonomous runs don't silently blow the context window. Hooks.OnIteration observes each iteration; the underlying Loop and ContextManager.Trim primitives stay available if you want a different policy. No persistence, scheduler, runner, or hidden lifecycle.

Built-in tools

Package Tools
tools/clock current UTC time
tools/math safe calculator
tools/workspace sandboxed list, find, search, read, file info, exact-string edit
clockTool := clock.New()
ws, err := workspace.NewReadOnly(workspace.Config{Root: "."})
toolset := gocode.MustJoin(clockTool.Toolset(), ws.Toolset())

workspace.NewReadOnly is read-only. workspace.New includes edit_file — wrap it with WithConfirmation before letting writes run.

Provider tools

Some tools live on the provider side: Anthropic and OpenAI ship a set of tools the model is already trained to use. They split into two shapes.

Server-executed (category 1): the provider runs the tool and returns the result inline. There is no Go function to write. Attach via ProviderTools:

import (
    "github.com/lukemuz/gocode"
    "github.com/lukemuz/gocode/providers/anthropic"
    "github.com/lukemuz/gocode/providers/openai"
)

// Anthropic — works against the standard Messages API.
toolset := gocode.Tools(myLocalBinding).
    WithProviderTools(
        anthropic.WebSearch(anthropic.WebSearchOpts{MaxUses: 3}),
        anthropic.CodeExecution(),
    )

// OpenAI Responses — needs openai.NewResponsesProvider.
toolset := gocode.Tools(myLocalBinding).
    WithProviderTools(
        openai.WebSearch(),
        openai.CodeInterpreter(openai.CodeInterpreterOpts{}),
        openai.FileSearch(openai.FileSearchOpts{VectorStoreIDs: []string{"vs_..."}}),
        openai.ImageGeneration(),
    )

The agent loop never dispatches these — the response carries provider-specific result items (server_tool_use, web_search_call, code_interpreter_call, …) that round-trip verbatim via ContentBlock.Raw.

Provider-defined schema, you execute (category 2): the model has been trained on the tool's name and arguments, but you supply the runtime — bash, text_editor, computer. The wire declaration is {type, name} instead of {name, description, input_schema}, and the dispatch flow is identical to a normal tool. Constructors return ordinary gocode.ToolBindings:

bash := anthropic.BashTool(func(ctx context.Context, in json.RawMessage) (string, error) {
    // run the model's command in your sandbox of choice
})
toolset := gocode.Tools(bash).Wrap(gocode.WithConfirmation(promptUser))

Tools and ProviderTools are tagged for one provider; passing them to a different one fails at request build with a clear error.

OpenAI: Chat Completions vs. Responses. Hosted tools (web_search, file_search, code_interpreter, image_generation) live on /v1/responses, not /v1/chat/completions. Use openai.NewResponsesClientFromEnv(model) (or build one from openai.NewResponsesProvider) when you want them. Plain function calling works on both endpoints; OpenAI has signaled Responses as the path forward, so prefer it for new code.

Prompt caching

Long, stable prompts (system instructions, tool definitions, big context blocks) can be cached so subsequent turns pay a fraction of the input-token cost. Caching is provider-specific in mechanism but exposed uniformly via gocode.CacheControl:

// The most common pattern: cache the system prompt and the tool prefix
// for any subsequent turn within the cache window.
client, _ := gocode.New(gocode.Config{
    Provider:    provider,
    Model:       gocode.ModelSonnet,
    SystemCache: gocode.Ephemeral(),       // 5-minute TTL
})
toolset := gocode.Tools(...).CacheLast(gocode.Ephemeral())

Per-provider behavior:

Provider Caching mechanism Honors markers?
anthropic.Provider Explicit cache_control blocks (cumulative; up to 4 breakpoints) Yes — system, tools, message blocks
openrouter.Provider Translates markers to OpenAI-compatible typed-parts content; routed through to Anthropic backends Yes — system, tools, message blocks
openai.Provider Automatic for prefixes ≥1024 tokens; no field needed Markers ignored (dropped before send)
openai.ResponsesProvider Automatic, same as Chat Completions Markers ignored

Use gocode.EphemeralExtended() for the 1-hour TTL when a prefix will be reused across long sessions.

Usage reports cache stats when the provider returns them — CacheCreationTokens (Anthropic only — tokens written to cache this turn) and CacheReadTokens (Anthropic and OpenAI/OpenRouter — tokens served from cache at a discount).

MCP

mcp adapts Model Context Protocol tools into ordinary toolsets.

srv, err := mcp.Connect(ctx, mcp.Config{Command: "my-mcp-server"})
defer srv.Close()
mcpTools, err := srv.Toolset(ctx)
result, err := client.Loop(ctx, system, history, mcpTools, 10)

You choose the server, inspect the tools, and pass them in.

Streaming

_, _, err := client.AskStream(ctx, system, history, func(delta gocode.ContentBlock) {
    if delta.Type == gocode.TypeText {
        fmt.Print(delta.Text)
    }
})

Use LoopStream or Agent.StepStream for streamed tool loops.

Retries can restart a stream after partial output, so callbacks may see partial text from failed attempts. Use StreamBuffer with RetryConfig.OnRetry to react and clear:

sb := gocode.NewStreamBuffer(
    func(b gocode.ContentBlock) { fmt.Print(b.Text) },
    func() { fmt.Print("\n[retrying…]\n") },
)
client, _ := gocode.New(gocode.Config{..., Retry: gocode.RetryConfig{OnRetry: sb.OnRetry}})
msg, _, err := client.AskStream(ctx, system, history, sb.OnToken)

Sessions

Session is plain data. You load it, pass History to a model call, and persist the result yourself.

sess, err := store.Get(ctx, sessionID)
if errors.Is(err, gocode.ErrSessionNotFound) {
    sess = &gocode.Session{ID: sessionID}
} else if err != nil {
    return err
}

sess.History = append(sess.History, gocode.NewUserMessage(input))
result, err := assistant.Step(ctx, sess.History)
if err != nil {
    return err
}
sess.History = result.Messages

if len(sess.History) == 1 {
    err = store.Create(ctx, sess)
} else {
    err = store.Update(ctx, sess)
}

Two built-in stores: MemoryStore (in-memory, concurrent-safe) and FileStore (one JSON file per session, atomic writes). Both implement:

type Store interface {
    Create(ctx context.Context, session *Session) error
    Get(ctx context.Context, id string) (*Session, error)
    Update(ctx context.Context, session *Session) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context, prefix string, limit int) ([]*Session, error)
}

Create returns ErrSessionExists; Update returns ErrSessionNotFound. Both work with errors.Is.

Errors and retries

client, err := gocode.New(gocode.Config{
    Provider:  provider,
    Model:     gocode.ModelSonnet,
    MaxTokens: 4096,
    Retry: gocode.RetryConfig{
        MaxRetries:  5,
        InitialWait: time.Second,
        MaxWait:     30 * time.Second,
        OnRetry: func(attempt int, wait time.Duration) {
            log.Printf("retry %d, waiting %s", attempt, wait)
        },
    },
})

Errors are typed and work with errors.Is / errors.As: APIError, ToolError, LoopError, RetryExhaustedError, ErrMissingTool, ErrMaxIter.

Tool execution errors are soft by default: the error returns to the model as a tool result with IsError: true. Missing tools are configuration errors.

Testing

The Provider interface is the main testing seam. You can test calls, loops, streaming, tool execution, history shape, usage, and errors without real API calls.

go test ./...

Good tests assert contracts (message order, tool calls, error types, callback order, usage accumulation), not exact LLM prose.

Examples

Smaller examples in examples/:

go run ./examples/ask        # one model call
go run ./examples/pipeline   # parallel + sequential composition
go run ./examples/agent      # tool-using loop
go run ./examples/stream     # streaming

Larger runnable patterns in examples/recipes/:

  • 02-agent-with-tools — curated toolset, middleware, context management
  • 03-repo-explainer — sandboxed workspace tools, streaming, file-backed sessions
  • 04-router-subagents — orchestrator delegates to specialist subagents
  • 05-persistent-chat — long-running conversation with FileStore
  • 06-parallel-pipeline — parallel fan-out then sequential fan-in
  • 07-web-service — deploy-shaped HTTP server (JSON + SSE) with a Dockerfile

Set the relevant API key first.

See VISION.md for design philosophy and ROADMAP.md for forward-looking work and what stays out of core.

Documentation

Index

Constants

View Source
const (
	ModelOpus   = "claude-opus-4-7"
	ModelSonnet = "claude-sonnet-4-6"
	ModelHaiku  = "claude-haiku-4-5-20251001"
)

Well-known Anthropic model identifiers.

View Source
const (
	RoleUser      = "user"
	RoleAssistant = "assistant"
)

Role constants for Message.

View Source
const (
	TypeText       = "text"
	TypeToolUse    = "tool_use"
	TypeToolResult = "tool_result"
	TypeImage      = "image"
)

Type constants for ContentBlock.

Variables

View Source
var (
	// ErrMaxIter is wrapped in a LoopError when Loop exhausts its iteration budget.
	ErrMaxIter = errors.New("gocode: loop exceeded maxIter")

	// ErrMissingTool is wrapped in a ToolError when the model calls a tool
	// that is not present in the dispatch map.
	ErrMissingTool = errors.New("gocode: model called unknown tool")

	// ErrRetryExhausted is wrapped in a RetryExhaustedError when all retry
	// attempts have been consumed without a successful response.
	ErrRetryExhausted = errors.New("gocode: retry exhausted")
)

Sentinel errors for use with errors.Is.

View Source
var (
	// ErrSessionNotFound is returned by Get, Update, and Delete when no
	// session with the requested ID exists.
	ErrSessionNotFound = errors.New("gocode: session not found")

	// ErrSessionExists is returned by Create when a session with the given
	// ID already exists.
	ErrSessionExists = errors.New("gocode: session already exists")
)

Sentinel errors for Store operations.

Functions

func AttachImage added in v0.1.2

func AttachImage(ctx context.Context, img ImageBlock)

AttachImage attaches an image to the current tool call's result. It is the mechanism by which a ToolFunc — whose return type is just a string — can ride image bytes back to the model alongside its textual output.

Pass a base64 data URI ("data:image/png;base64,...") for portability; remote http URLs are accepted but not all backends will fetch them.

AttachImage is a no-op when called outside the agent loop (e.g. in a unit test that invokes a ToolFunc directly with a vanilla context).

func GetState

func GetState[T any](s *Session, key string) (T, error)

GetState unmarshals the JSON value stored under key in s.State into a value of type T. Returns an error if the key is absent or the value cannot be decoded into T.

func JSONResult

func JSONResult(v any) (string, error)

JSONResult marshals v to a JSON string. It is the recommended helper for typed tool handlers that want to return structured data the model can reliably parse. Marshal errors are wrapped with context.

func NewTypedTool

func NewTypedTool[Input any](
	name, description string,
	schema InputSchema,
	f func(context.Context, Input) (string, error),
) (Tool, ToolFunc)

NewTypedTool combines NewTool + TypedToolFunc into a single call that returns both the Tool (for your tools []Tool slice) and the wrapped ToolFunc (for your dispatch map). This is the most ergonomic path shown in the roadmap while still letting you inspect everything.

Panics if schema cannot be marshalled to JSON; in practice InputSchema always marshals successfully, so this is a programmer-error indicator.

func RenderForSummary

func RenderForSummary(msgs []Message, maxToolBytes int) string

RenderForSummary flattens a slice of messages into a plain-text transcript suitable for passing to a summarizer model. Each message is rendered as labeled lines (USER, ASSISTANT, ASSISTANT_TOOL_USE, TOOL_RESULT) that preserve the structure without paying the full token cost of large tool outputs: tool_use inputs and tool_result contents are abbreviated to maxToolBytes characters with a "...[truncated]" marker. Pass 0 for maxToolBytes to use the default of 400.

This is the rendering most ContextManager.Summarizer implementations want. If you need different formatting (different abbreviation thresholds, JSON output, redaction), write your own — the message structures are public.

func Save

func Save(ctx context.Context, store Store, s *Session) error

Save updates s if it already exists, or creates it if not. It is a convenience wrapper for callers that do not need to distinguish between first use and subsequent calls.

func SessionExists

func SessionExists(id string) error

SessionExists returns an error that wraps ErrSessionExists with the offending ID, the create-side counterpart to SessionNotFound.

func SessionNotFound

func SessionNotFound(id string) error

SessionNotFound returns an error that wraps ErrSessionNotFound with the offending ID. Store implementations use this to produce errors that match errors.Is(err, ErrSessionNotFound) while still naming the missing ID.

func SetState

func SetState[T any](s *Session, key string, val T) error

SetState marshals val as JSON and stores it under key in s.State.

func TextContent

func TextContent(msg Message) string

TextContent extracts and concatenates all text blocks from a message.

func WithImageSink added in v0.1.2

func WithImageSink(parent context.Context) (context.Context, func() []ImageBlock)

WithImageSink installs a fresh image sink on parent and returns the derived context plus a drain function that yields (and clears) any images attached during the call. This is a testing aid: image-emitting ToolFuncs can be exercised without standing up a full Loop. The agent loop installs an equivalent sink implicitly per tool dispatch.

Types

type APIError

type APIError struct {
	StatusCode int
	Type       string
	Message    string
	// RetryAfter, when non-zero, carries the duration requested by the API via
	// a Retry-After header (e.g. on a 429 Too Many Requests response).
	RetryAfter time.Duration
}

APIError is returned when the LLM API responds with a non-2xx status.

func (*APIError) Error

func (e *APIError) Error() string

type Agent

type Agent struct {
	// Client is the LLM client used for all model calls. Required.
	Client *Client

	// System is the system prompt passed to every model call.
	System string

	// Tools is the set of tools advertised to the model and dispatched when
	// called. A zero-value Toolset means no tools are offered.
	Tools Toolset

	// Context trims history before the first model call and again before each
	// subsequent model call inside the loop. A zero-value ContextManager
	// (MaxTokens == 0) disables trimming entirely.
	Context ContextManager

	// MaxIter caps the number of model calls per Step. Zero means no limit.
	MaxIter int

	// Hooks contains optional observer callbacks. A zero-value Hooks is safe.
	Hooks Hooks
}

Agent is an assembled primitive: a Client, system prompt, Toolset, ContextManager, and optional Hooks wired together into a single Step call. It is the practical assembly point for a tool-using agent — for one-shot autonomous tasks pass a single user message containing the goal; for multi-turn conversations call Step once per human turn.

Step trims history before the loop and again before every model call inside the loop (when ContextManager.MaxTokens > 0), so long autonomous runs do not blow the context window. Tool-use / tool-result integrity is preserved by ContextManager.Trim.

A zero-value Agent is not valid — Client must be set. All other fields have safe zero values (no tools, no context trimming, no iteration limit, no hooks).

Usage:

a := gocode.Agent{
    Client:  client,
    System:  "You are a helpful assistant.",
    Tools:   myToolset,
    Context: gocode.ContextManager{MaxTokens: 8000, KeepRecent: 20},
    MaxIter: 10,
}

// One-shot task: pass a single user message with the goal.
result, err := a.Step(ctx, []gocode.Message{gocode.NewUserMessage("do the thing")})
fmt.Println(result.FinalText())

// Multi-turn: call Step once per human turn, threading history.
history := []gocode.Message{gocode.NewUserMessage("first question")}
result, err = a.Step(ctx, history)
history = result.Messages

func (Agent) Step

func (a Agent) Step(ctx context.Context, history []Message) (LoopResult, error)

Step runs one user request through the agent loop. history is the conversation so far; it is not modified. The returned LoopResult contains the full updated conversation (trimmed history + new turns) and aggregate token usage for this step.

Step trims history once up front (so OnStep sees the iter-0 history) and then again before every model call inside the loop when a ContextManager is configured. The primitives Loop and ContextManager.Trim remain available for callers who want a different policy.

func (Agent) StepStream

func (a Agent) StepStream(
	ctx context.Context,
	history []Message,
	onToken func(ContentBlock),
	onToolResult func([]ToolResult),
) (LoopResult, error)

StepStream is the streaming variant of Step. It delivers token deltas via onToken and tool results via onToolResult as they arrive. Both callbacks may be nil.

Retry interaction: onToken may fire for partial content on a failed attempt before a successful retry. Wire a StreamBuffer via RetryConfig.OnRetry to reset partial output between attempts.

type CacheControl

type CacheControl struct {
	Type string `json:"type"`          // always "ephemeral"
	TTL  string `json:"ttl,omitempty"` // "" (default 5m) or "1h"
}

CacheControl marks a content block, tool definition, or system prompt as a cache breakpoint. The semantics are Anthropic's: caching is cumulative — a marker on, say, the last tool definition caches the system prompt and every preceding tool. Up to 4 markers per request.

Providers translate this marker as appropriate:

  • AnthropicProvider emits native cache_control blocks.
  • OpenRouterProvider serializes content as a typed-parts array with cache_control fields; works for Anthropic-backed routes.
  • OpenAIProvider and OpenAIResponsesProvider ignore the marker — OpenAI caches automatically for prefixes ≥1024 tokens.

Set TTL to "1h" for the extended (more expensive write, longer-lived) tier; leave empty for the default 5-minute window.

func Ephemeral

func Ephemeral() *CacheControl

Ephemeral returns the standard 5-minute cache marker. It is a thin constructor that documents intent at call sites.

func EphemeralExtended

func EphemeralExtended() *CacheControl

EphemeralExtended returns a 1-hour cache marker. Cache writes cost more at this tier but reads remain cheap, so it pays off for prompts reused across long sessions or many users.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a stateless API facade. It holds configuration but no conversation state — history is owned by the caller. The same Client is safe for concurrent use across goroutines.

func New

func New(cfg Config) (*Client, error)

New creates a Client from cfg, filling in defaults for zero-value fields. Returns an error if Provider or Model is empty.

func (*Client) Ask

func (c *Client) Ask(ctx context.Context, system string, history []Message) (Message, Usage, error)

Ask makes a single LLM call and returns the model's reply along with token usage for the call.

system sets the system prompt; pass "" to omit it. history is the conversation so far and is not modified by Ask. Append the returned Message to your history slice to continue the conversation.

Token usage is reported separately so callers can aggregate cost across multiple Ask calls without paying for a Loop. Pre-1.0 note: Ask previously returned (Message, error); the Usage return was added so cost-conscious callers don't have to fall back to Loop with an empty toolset.

func (*Client) AskStream

func (c *Client) AskStream(ctx context.Context, system string, history []Message, onToken func(ContentBlock)) (Message, Usage, error)

AskStream is the streaming variant of Ask. It invokes the onToken callback for every ContentBlock delta delivered by the provider (typically incremental TypeText blocks). The final assembled Message is returned once the stream completes. history is not modified by this call.

Retry interaction: callWithRetry wraps the stream call, so onToken may fire for partial content on a failed attempt before a successful retry begins. Use StreamBuffer with RetryConfig.OnRetry to react to retries and clear partial output before the next attempt starts.

onToken may be nil, in which case token deltas are discarded.

Pre-1.0 note: AskStream previously returned (Message, error); Usage was added so streaming callers don't have to drop down to Loop for cost tracking.

func (*Client) Loop

func (c *Client) Loop(
	ctx context.Context,
	system string,
	history []Message,
	tools Toolset,
	maxIter int,
) (LoopResult, error)

Loop runs the agent in a tool-use loop until the model signals end_turn or an error occurs. It returns the full conversation including all new turns.

tools is the Toolset advertised to the model on every call. A tool name that appears in a model response but is absent from the toolset's dispatch map causes an immediate LoopError wrapping ErrMissingTool. maxIter caps the total number of API calls; 0 means no limit.

The Toolset is read once: its Tools() and Dispatch() are computed at the start of the loop. Mutating the underlying Bindings during the loop has no effect on iterations already in flight.

func (*Client) LoopStream

func (c *Client) LoopStream(
	ctx context.Context,
	system string,
	history []Message,
	tools Toolset,
	maxIter int,
	onToken func(ContentBlock),
	onToolResult func([]ToolResult),
) (LoopResult, error)

LoopStream is the streaming variant of Loop. It mirrors Loop's structure, control flow, error handling, stop-reason switching, maxIter limiting, and history/usage accumulation but invokes Provider.Stream (wrapped by callWithRetry) on every iteration so that onToken receives each ContentBlock delta as it arrives. After runTools completes, onToolResult is called with the results (allowing live UI updates or logging) before the tool results are appended and the loop continues.

Retry interaction: onToken may fire multiple times for a given turn if retries occur. Use StreamBuffer with RetryConfig.OnRetry to react to retries and clear partial output before the next attempt starts.

Both callbacks may be nil, in which case their respective events are discarded.

func (*Client) WithModel

func (c *Client) WithModel(model string) *Client

WithModel returns a new Client that shares the provider, MaxTokens, and Retry config with c, but uses a different model. Useful for cost-tiering — a cheap summarizer alongside a smart loop, for example. The returned Client is independent: mutations to one do not affect the other.

For more elaborate derivation (different MaxTokens, different Retry), construct a fresh Client with gocode.New.

func (*Client) WithRecorder

func (c *Client) WithRecorder(rec Recorder) *Client

WithRecorder returns a new Client that shares the rest of c's config but replaces the Recorder. Pass nil to disable recording on the derived Client. The returned Client is independent of c.

type Config

type Config struct {
	Provider  Provider    // required — the LLM backend to use
	Model     string      // required — provider-specific model identifier
	MaxTokens int         // max tokens per response; defaults to 1024
	Retry     RetryConfig // controls automatic retry behaviour for transient API errors

	// Recorder, if non-nil, receives Events as Loop / LoopStream runs:
	// turn start/end, model request/response, retry attempts, and tool
	// call start/end. See recorder.go for event semantics. Recording is
	// best-effort and must not block the loop; implementations should be
	// fast and non-blocking. Ask and AskStream do not emit events.
	Recorder Recorder

	// SystemCache, when set, marks the system prompt as a cache breakpoint
	// on every API call this Client makes via Ask, AskStream, Loop, and
	// LoopStream. The most useful single cache placement when the system
	// text is long and stable across turns.
	SystemCache *CacheControl
}

Config holds everything needed to create a Client.

type ContentBlock

type ContentBlock struct {
	Type      string          `json:"type"`
	Text      string          `json:"text,omitempty"`
	ID        string          `json:"id,omitempty"`
	Name      string          `json:"name,omitempty"`
	Input     json.RawMessage `json:"input,omitempty"`
	ToolUseID string          `json:"tool_use_id,omitempty"`
	Content   string          `json:"content,omitempty"`
	IsError   bool            `json:"is_error,omitempty"`

	// Image fields. Populated when Type=="image". Source is either a
	// base64 data URI ("data:image/png;base64,...") or an http(s) URL;
	// MediaType is the IANA media type ("image/png", "image/jpeg", ...).
	// Caller is responsible for pre-encoding bytes; gocode does not
	// downsample or re-encode.
	Source    string `json:"source,omitempty"`
	MediaType string `json:"media_type,omitempty"`

	// CacheControl, if set, marks this block as a cache breakpoint. Caching
	// is cumulative — see the CacheControl docs. Currently honored by
	// AnthropicProvider and OpenRouterProvider; ignored by other providers.
	CacheControl *CacheControl `json:"cache_control,omitempty"`

	// Raw, if non-empty, is the verbatim JSON for this block. Set by
	// UnmarshalJSON for unknown (provider-specific) types so they can be
	// resent on the next request without loss. When set, MarshalJSON emits
	// Raw and ignores all other fields.
	Raw json.RawMessage `json:"-"`
}

ContentBlock is one element in a message's content array. Each block has a Type that determines which other fields are populated:

  • "text": Text is the response string
  • "tool_use": ID, Name, and Input carry the model's tool call
  • "tool_result": ToolUseID and Content carry the tool's return value

Provider-specific block types (e.g. Anthropic's "server_tool_use" or "web_search_tool_result", emitted when category-1 provider tools run) are preserved opaquely in Raw and round-trip verbatim. The agent loop ignores them — only Type=="tool_use" is dispatched locally.

func (ContentBlock) MarshalJSON

func (b ContentBlock) MarshalJSON() ([]byte, error)

MarshalJSON emits Raw verbatim if set; otherwise emits the standard fields.

func (*ContentBlock) UnmarshalJSON

func (b *ContentBlock) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes known block types into typed fields. For unknown types it captures the entire JSON object into Raw so the block can be re-sent verbatim on subsequent requests (Anthropic requires server-tool result blocks to round-trip exactly).

type ContextManager

type ContextManager struct {
	// MaxTokens is the token budget. Trim returns history unchanged if it
	// fits within this budget. Zero disables trimming entirely.
	MaxTokens int

	// KeepFirst is the number of messages to always retain from the start of
	// history regardless of token budget — useful for preserving an initial
	// instruction or persistent context. The actual number kept may be larger
	// when preserving tool-use/tool-result integrity requires it.
	KeepFirst int

	// KeepRecent is the number of messages to always retain from the end of
	// history. The actual number kept may be larger when integrity requires
	// including the full tool cycle that contains the boundary message.
	KeepRecent int

	// TokenCounter estimates the token count of a message slice. If nil, a
	// character-based heuristic (4 chars ≈ 1 token) is used. Provide a
	// model-specific counter for accurate budget enforcement.
	TokenCounter func([]Message) (int, error)

	// Summarizer condenses trimmed messages into a single string. When set,
	// the trimmed portion is replaced by a user message containing that string.
	// When nil, trimmed messages are dropped without replacement.
	//
	// The summarizer typically calls the LLM. That call is caller-owned and
	// visible: no hidden model calls occur inside Trim.
	Summarizer func(ctx context.Context, trimmed []Message) (string, error)
}

ContextManager trims conversation history to keep it within a token budget. A zero-value ContextManager is a no-op: MaxTokens == 0 disables trimming.

Typical usage:

cm := gocode.ContextManager{
    MaxTokens:  8000,
    KeepFirst:  1,   // always keep the initial user message
    KeepRecent: 10,  // always keep the last 10 messages
}
trimmed, err := cm.Trim(ctx, history)
result, err  := client.Loop(ctx, system, trimmed, tools, 10)

func (ContextManager) Trim

func (m ContextManager) Trim(ctx context.Context, history []Message) ([]Message, error)

Trim returns a copy of history that fits within MaxTokens. The original slice is never modified. If MaxTokens is zero or history is already within budget, history is returned as-is.

Trimming always preserves tool-use/tool-result integrity: an assistant message that contains tool_use blocks and the user message that carries the corresponding tool_results are always kept or dropped as a unit — the model must never see one without the other.

If KeepFirst > 0, the first KeepFirst messages are pinned. If KeepRecent > 0, the last KeepRecent messages are pinned. Both boundaries are expanded as needed to land on a clean cut point. Everything between the two pinned regions is the trim zone.

If Summarizer is set, the trim zone is passed to it and the returned string becomes a single user message inserted in place of the trimmed content. Otherwise the trim zone is dropped entirely.

Trim does not iterate: it removes the trim zone in one pass. If the result is still over budget (because KeepFirst + KeepRecent alone exceeds MaxTokens), the history is returned as trimmed without error — the caller will encounter a max_tokens API error and can decide how to respond.

type Event

type Event struct {
	Seq        int64     `json:"seq"`
	TurnID     string    `json:"turn_id"`
	Iter       int       `json:"iter"`
	Type       EventType `json:"type"`
	Time       time.Time `json:"time"`
	History    []Message `json:"history,omitempty"`     // TurnStart
	Message    *Message  `json:"message,omitempty"`     // ModelResponse
	Usage      *Usage    `json:"usage,omitempty"`       // ModelResponse, TurnEnd
	StopReason string    `json:"stop_reason,omitempty"` // ModelResponse

	ToolUseID  string          `json:"tool_use_id,omitempty"` // ToolCallStart, ToolCallEnd
	ToolName   string          `json:"tool_name,omitempty"`   // ToolCallStart, ToolCallEnd
	ToolInput  json.RawMessage `json:"tool_input,omitempty"`  // ToolCallStart
	ToolOutput string          `json:"tool_output,omitempty"` // ToolCallEnd
	ToolError  string          `json:"tool_error,omitempty"`  // ToolCallEnd (when IsError)
	IsError    bool            `json:"is_error,omitempty"`    // ToolCallEnd

	Attempt int           `json:"attempt,omitempty"` // RetryAttempt
	Wait    time.Duration `json:"wait,omitempty"`    // RetryAttempt
	Err     string        `json:"err,omitempty"`     // TurnError
}

Event is one record in a turn's activity log. Fields are populated based on Type — see EventType constants for which fields are set when. Event is designed to be JSON-friendly so a Recorder can serialize it directly.

Seq is assigned by the Recorder when Record is called and is monotonic per Recorder. TurnID is stable for all events in a single Loop / LoopStream invocation so events from concurrent turns can be separated. Iter is the 0-based iteration within the turn (the i-th model call). For tool events Iter matches the iteration whose tool_use block produced the call, so all tool events from one parallel batch share Iter and are ordered by Seq.

type EventType

type EventType string

EventType identifies what happened in an Event. The set is closed: every event emitted by Loop, LoopStream, runTools, and callWithRetry has one of these types.

const (
	// EventTurnStart is emitted once at the beginning of Loop / LoopStream,
	// before any model call. History carries the trimmed history about to be
	// sent.
	EventTurnStart EventType = "turn_start"

	// EventModelRequest is emitted before each model call. Iter is the 0-based
	// iteration within the turn.
	EventModelRequest EventType = "model_request"

	// EventModelResponse is emitted after each successful model call (after
	// retries, if any). Message holds the assistant content; Usage holds the
	// per-call token usage; StopReason carries the model's stop reason.
	EventModelResponse EventType = "model_response"

	// EventRetryAttempt is emitted before each retry sleep, mirroring
	// RetryConfig.OnRetry but routed through the Recorder. Attempt is the
	// 1-based retry number; Wait is the computed backoff.
	EventRetryAttempt EventType = "retry_attempt"

	// EventToolCallStart is emitted just before a tool dispatch begins.
	// Concurrent tool calls share the same Iter; Seq orders them.
	EventToolCallStart EventType = "tool_call_start"

	// EventToolCallEnd is emitted after a tool dispatch returns. ToolOutput
	// carries the result string; if the tool returned an error, ToolError
	// holds the error message and IsError is true.
	EventToolCallEnd EventType = "tool_call_end"

	// EventTurnEnd is emitted on successful turn completion (model returned
	// end_turn).
	EventTurnEnd EventType = "turn_end"

	// EventTurnError is emitted when Loop / LoopStream returns a non-nil
	// error. Err carries the error message.
	EventTurnError EventType = "turn_error"
)

type ExtractParams

type ExtractParams[T any] struct {
	// Description is the submit tool's description shown to the model. It is
	// the model-facing instruction for what value to produce. Required.
	Description string

	// Schema describes the shape of T. Required. Construct with the schema
	// helpers (Object, Array, ObjectOf, ...) or hand-roll JSON via
	// InputSchema literals.
	Schema InputSchema

	// Tools are additional tools the model may call before submitting (e.g.
	// search, retrieval). Pass a zero-value Toolset for pure structured output.
	Tools Toolset

	// Name is the submit tool's name. Defaults to "submit".
	Name string

	// MaxIter caps the number of model turns. Defaults to 8.
	MaxIter int

	// Validate, if non-nil, is called with the model's submitted value before
	// Extract accepts it. Returning a non-nil error marks the submit as failed:
	// the model sees an is_error=true tool result with the error message and
	// may retry within the iteration budget. Use this to enforce constraints
	// the schema cannot express (length caps, cross-field invariants, etc.).
	Validate func(T) error
}

ExtractParams configures Extract. Description and Schema are required; the rest have sensible defaults.

The struct is generic over T so Validate can take a typed argument without the caller writing assertions. Most call sites pass a single literal:

gocode.Extract(ctx, client, system, history, gocode.ExtractParams[MyPlan]{
    Description: "Submit the final plan",
    Schema:      planSchema,
})

type Hooks

type Hooks struct {
	// OnStep is called once per Step / StepStream, after the initial context
	// trim and before the loop, with the trimmed history that will be sent
	// to the model on the first iteration. Useful for logging effective
	// context size or recording that a step started. Not called when Trim
	// returns an error.
	OnStep func(ctx context.Context, history []Message)

	// OnIteration is called before every model call inside the loop, with
	// the zero-based iteration index and the history that will actually be
	// sent (post-trim, when a ContextManager is configured). Useful for
	// observing long autonomous runs without dropping to Recorder.
	OnIteration func(ctx context.Context, iter int, history []Message)

	// OnStepDone is called after the loop returns, with the full LoopResult
	// and any error. Called even when the loop returns an error so callers
	// can record failures uniformly. Not called when the initial Trim fails.
	OnStepDone func(ctx context.Context, result LoopResult, err error)
}

Hooks contains optional observer callbacks for an Agent step. All fields are nil by default and are safe to leave unset. A zero-value Hooks struct disables all observation.

Call order within a Step or StepStream call:

  1. Context.Trim is called once on the input history.
  2. OnStep is called with the trimmed history (skipped if Trim fails).
  3. The internal loop runs. Before every model call inside the loop, Context.Trim is re-applied (when MaxTokens > 0) and then OnIteration is invoked with the iteration index and the trimmed history.
  4. OnStepDone is called with the result and any error from the loop.

OnStepDone is NOT called when the initial Trim fails — only when the loop itself returns. Per-tool-call observation is available via Config.Recorder (see EventToolCallStart / EventToolCallEnd).

type ImageBlock added in v0.1.2

type ImageBlock struct {
	Source    string
	MediaType string
}

ImageBlock is the caller-facing representation of an image attachment. Source is either a base64 data URI ("data:image/png;base64,...") or an http(s) URL; MediaType is the IANA media type ("image/png", ...).

type InputSchema

type InputSchema struct {
	Type       string                    `json:"type"`
	Properties map[string]SchemaProperty `json:"properties"`
	Required   []string                  `json:"required,omitempty"`
}

InputSchema is the JSON Schema object describing a tool's input parameters. The Anthropic API requires Type to be "object". Use the schema builder helpers below for ergonomic construction; the output is identical to a hand-written literal.

func Object

func Object(fields ...Property) InputSchema

Object builds an InputSchema from Properties. This is the main entry point for ergonomic tool schemas. The result is identical to a manual InputSchema literal and works with NewTool/NewTypedTool.

type JSONLRecorder

type JSONLRecorder struct {
	// contains filtered or unexported fields
}

JSONLRecorder writes one JSON object per line to the underlying writer. Errors from Write are silently dropped — recording is best-effort and must not interfere with the agent's main control flow. Wrap the writer yourself if you want error visibility.

func NewJSONLRecorder

func NewJSONLRecorder(w io.Writer) *JSONLRecorder

NewJSONLRecorder returns a JSONLRecorder writing to w.

func (*JSONLRecorder) Record

func (r *JSONLRecorder) Record(_ context.Context, ev Event)

Record marshals ev to JSON and writes it as one line.

type Logger

type Logger interface {
	Info(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger is the logging interface used by WithLogging. *slog.Logger satisfies it. Applications may supply any compatible implementation.

type LoopError

type LoopError struct {
	Iter  int
	Cause error
}

LoopError is returned when Loop exits without reaching end_turn. Iter is the loop iteration count at the point of failure.

func (*LoopError) Error

func (e *LoopError) Error() string

func (*LoopError) Unwrap

func (e *LoopError) Unwrap() error

type LoopResult

type LoopResult struct {
	Messages []Message // full conversation: original history + all new turns
	Usage    Usage     // total tokens consumed across all iterations
}

LoopResult is returned by Loop and carries the complete updated history together with aggregate token usage across all API calls in the run.

func Extract

func Extract[T any](
	ctx context.Context,
	client *Client,
	system string,
	history []Message,
	params ExtractParams[T],
) (T, LoopResult, error)

Extract runs a tool-use loop in which the model is required to call a single terminal "submit" tool whose typed argument becomes the return value.

This collapses the "submit_X" structured-output pattern into one call. Both pure structured output (no other tools) and structured output via tool use (e.g. search-then-submit) are expressed by setting Params.Tools.

On success, the returned T is the accepted value and LoopResult contains the full conversation (including the terminal tool call) and aggregate usage.

Errors:

  • The underlying loop fails (network, max_tokens, max_iter, etc.).
  • The model ends its turn without ever calling the submit tool — Extract returns the zero value of T and an error mentioning the tool name.

Validation failures are NOT errors — they are surfaced to the model as retriable tool errors. Only when the loop ultimately ends without an accepted submission does Extract return an error.

func (LoopResult) Final

func (r LoopResult) Final() Message

Final returns the last message in Messages, which is conventionally the final assistant reply. Returns the zero Message if Messages is empty.

func (LoopResult) FinalText

func (r LoopResult) FinalText() string

FinalText returns the concatenated text of the final assistant message. Equivalent to TextContent(r.Final()). Returns "" if no messages are present.

type MemoryRecorder

type MemoryRecorder struct {
	// contains filtered or unexported fields
}

MemoryRecorder collects events in memory. Useful for tests and for the open-or-create chat pattern where events round-trip through Session.Events.

func NewMemoryRecorder

func NewMemoryRecorder() *MemoryRecorder

NewMemoryRecorder returns an empty MemoryRecorder.

func (*MemoryRecorder) Events

func (r *MemoryRecorder) Events() []Event

Events returns a copy of the recorded events in order.

func (*MemoryRecorder) Record

func (r *MemoryRecorder) Record(_ context.Context, ev Event)

Record appends ev to the in-memory log, assigning a monotonic Seq.

func (*MemoryRecorder) Reset

func (r *MemoryRecorder) Reset()

Reset clears the recorder. Seq numbering restarts at 1.

type Message

type Message struct {
	Role    string         `json:"role"`
	Content []ContentBlock `json:"content"`
}

Message is one turn in a conversation.

func NewToolResultMessage

func NewToolResultMessage(results []ToolResult) Message

NewToolResultMessage builds the user-role turn that returns tool outputs to the model after a tool_use response. Image attachments collected on any ToolResult are flattened onto the same canonical message after the tool_result blocks; the wire serializer splits them into a sibling role="user" message when sending.

func NewUserMessage

func NewUserMessage(text string) Message

NewUserMessage creates a plain-text user turn.

func NewUserMessageWithImages added in v0.1.2

func NewUserMessageWithImages(text string, images []ImageBlock) Message

NewUserMessageWithImages builds a user-role turn carrying a text block followed by one image content block per image. Pass an empty text to emit an image-only turn (the leading text block is skipped).

type Middleware

type Middleware func(binding ToolBinding) ToolFunc

Middleware is a function that wraps a ToolBinding's Func with additional behaviour. The full ToolBinding (including Tool, Meta, and the current Func) is passed so wrappers can use the tool name, metadata, and safety notes. The wrapper must return a new ToolFunc; it must not mutate the binding.

func WithConfirmation

func WithConfirmation(prompt func(ctx context.Context, binding ToolBinding, input json.RawMessage) (bool, error)) Middleware

WithConfirmation returns a Middleware that calls prompt before each tool invocation. If prompt returns false the tool is skipped and a descriptive message is returned to the model instead of executing the tool. If prompt returns an error that error becomes a hard tool error and is surfaced to the caller as usual.

func WithLogging

func WithLogging(logger Logger) Middleware

WithLogging returns a Middleware that logs each tool call and its result at Info level, or Error level on failure, using the supplied Logger. Pass a *slog.Logger (or any Logger-compatible value) as the argument.

func WithPanicRecovery

func WithPanicRecovery() Middleware

WithPanicRecovery returns a Middleware that recovers from panics inside a ToolFunc, converting them into ordinary errors so the agent loop can continue. This is useful when wrapping untrusted or third-party tool implementations.

func WithResultLimit

func WithResultLimit(maxBytes int) Middleware

WithResultLimit returns a Middleware that truncates tool output to at most maxBytes bytes. This prevents unexpectedly large tool results from filling the model's context window.

func WithTimeout

func WithTimeout(d time.Duration) Middleware

WithTimeout returns a Middleware that cancels a tool call if it exceeds d.

type MultiRecorder

type MultiRecorder []Recorder

MultiRecorder fans Record calls out to several recorders in order. Each child assigns its own Seq independently.

func (MultiRecorder) Record

func (m MultiRecorder) Record(ctx context.Context, ev Event)

Record forwards ev to every child recorder.

type Option

type Option func(*Property)

Schema builder helpers follow the design principles (explicit core, simple assembly, progressive disclosure, Lego-like tools, agent-legible by design). They are pure convenience: no reflection, no hidden state, fully inspectable output (Property and InputSchema are plain structs). Users can always fall back to writing InputSchema by hand. Matches the ROADMAP.md examples and compiles down to the primitives.

Example:

schema := Object(
	String("path", "Path to read", Required()),
	Number("limit", "Max items to return", Enum(10, 25, 50)),
)

func Enum

func Enum(values ...any) Option

Enum returns an Option that adds an enum constraint to the property. The values appear directly in the JSON schema.

func Required

func Required() Option

Required returns an Option that marks this property as required in the generated InputSchema.

type Property

type Property struct {
	Name string
	SchemaProperty
	Required bool
}

Property represents one field for Object(). All fields are exported so the constructed schema remains fully transparent and inspectable.

func Array

func Array(name, description string, items SchemaProperty, opts ...Option) Property

Array returns a Property with type "array". items describes the element schema; use a primitive SchemaProperty (e.g. {Type: "string"}) for arrays of scalars or ObjectOf(...) for arrays of objects.

Example:

Array("tags", "Free-form tags", SchemaProperty{Type: "string"})
Array("steps", "Pipeline steps", ObjectOf(
    String("name", "Step name", Required()),
    Integer("retries", "Retry count"),
), Required())

func Boolean

func Boolean(name, description string, opts ...Option) Property

Boolean returns a Property with type "boolean".

func Integer

func Integer(name, description string, opts ...Option) Property

Integer returns a Property with type "integer".

func Number

func Number(name, description string, opts ...Option) Property

Number returns a Property with type "number".

func String

func String(name, description string, opts ...Option) Property

String returns a Property with type "string".

type Provider

type Provider interface {
	// Call sends a single request and returns a normalised response.
	Call(ctx context.Context, req ProviderRequest) (ProviderResponse, error)

	// Stream sends the request and invokes onDelta for every incremental
	// ContentBlock (typically text deltas; also partial tool_use blocks
	// when supported by the backend). It returns the final aggregated
	// ProviderResponse (complete Content, StopReason, and Usage) once
	// the stream ends. The callback is invoked synchronously as data
	// arrives. Retries (if configured) may cause multiple callback
	// invocations across attempts.
	Stream(ctx context.Context, req ProviderRequest, onDelta func(ContentBlock)) (ProviderResponse, error)
}

Provider is the abstraction over an LLM API backend. Implementations translate between the canonical ProviderRequest / ProviderResponse types and their own wire formats.

type ProviderRequest

type ProviderRequest struct {
	Model         string
	MaxTokens     int
	System        string
	Messages      []Message
	Tools         []Tool
	ProviderTools []ProviderTool

	// SystemCache, if set, marks the system prompt as a cache breakpoint.
	// This is the most useful single cache placement when the system text
	// is large and stable across turns. Honored by AnthropicProvider and
	// OpenRouterProvider; ignored elsewhere (OpenAI caches automatically).
	SystemCache *CacheControl
}

ProviderRequest is the canonical request passed to every Provider.

Tools are local (category-2 included) tool declarations the model may call; the provider translates them to its native wire format and the agent loop dispatches via ToolFunc when the model emits a tool_use block.

ProviderTools are server-executed (category-1) tools — the provider runs them and returns inline result blocks. The loop never inspects them; they are passed straight through and spliced verbatim into the provider's tools array. Each entry is tagged with a Provider string so providers can reject mismatched entries at request build time.

type ProviderResponse

type ProviderResponse struct {
	Content    []ContentBlock
	StopReason string
	Usage      Usage
}

ProviderResponse is the normalised response every Provider must return.

type ProviderTool

type ProviderTool struct {
	Provider string          // e.g. "anthropic"
	Raw      json.RawMessage // verbatim JSON spliced into the provider's tools array
}

ProviderTool is a server-executed (category-1) tool advertised to the model. The provider runs it; no Go ToolFunc is needed. The agent loop never inspects ProviderTools — they pass through ProviderRequest to the provider, which splices Raw verbatim into its native tools array. Provider tags the entry to a specific provider so misuse fails at request-build time.

Use the typed constructors (AnthropicWebSearch, AnthropicCodeExecution, ...) to build these rather than instantiating the struct directly.

type Recorder

type Recorder interface {
	Record(ctx context.Context, ev Event)
}

Recorder receives Events as they happen during Loop / LoopStream. Implementations must be safe for concurrent use because runTools dispatches tool calls in parallel and each goroutine emits its own start/end events.

The Seq field on the incoming event is ignored; the Recorder assigns its own monotonic Seq before storing or forwarding. This keeps Seq meaningful even when multiple Loops share one Recorder.

func RecorderToSession

func RecorderToSession(sess *Session) Recorder

RecorderToSession returns a Recorder that appends to sess.Events. The returned Recorder is safe for concurrent use; events accumulate on the session in record order so a subsequent Save persists the full log.

type Result

type Result[T any] struct {
	Value T
	Err   error
}

Result carries the outcome of one parallel step.

func Parallel

func Parallel[T any](ctx context.Context, steps ...StepFunc[T]) []Result[T]

Parallel runs all steps concurrently and waits for all to finish. The returned slice is index-aligned: results[i] corresponds to steps[i].

No step is cancelled if another fails — cancellation policy is the caller's responsibility. Callers who want fail-fast behaviour should pass a derived context with a cancel func and call it after checking results for errors.

type RetryConfig

type RetryConfig struct {
	MaxRetries  int           // max attempts after the first; 0 → use default (3)
	InitialWait time.Duration // first back-off interval; 0 → use default (1s)
	MaxWait     time.Duration // back-off ceiling; 0 → use default (30s)
	Disabled    bool          // set true to disable all retrying

	// OnRetry, when non-nil, is called before each retry sleep with the
	// 1-based retry attempt number and the computed backoff duration.
	// Use this to log retries or reset streaming state (see StreamBuffer).
	OnRetry func(attempt int, wait time.Duration)
}

RetryConfig controls automatic retry behaviour for transient API errors. The zero value enables retrying with sensible defaults.

type RetryExhaustedError

type RetryExhaustedError struct {
	Attempts int
	Cause    error
}

RetryExhaustedError is returned by callWithRetry when every attempt has failed and no more retries remain. Attempts is the total number of calls that were made (including the initial attempt). Cause is the last error returned by the underlying function.

func (*RetryExhaustedError) Error

func (e *RetryExhaustedError) Error() string

func (*RetryExhaustedError) Unwrap

func (e *RetryExhaustedError) Unwrap() []error

Unwrap returns the last error that caused retries to be exhausted, enabling errors.Is / errors.As to inspect the underlying failure. It also chains ErrRetryExhausted so callers can match on errors.Is(err, ErrRetryExhausted).

type SchemaProperty

type SchemaProperty struct {
	Type        string                    `json:"type"`
	Description string                    `json:"description,omitempty"`
	Enum        []any                     `json:"enum,omitempty"`
	Items       *SchemaProperty           `json:"items,omitempty"`      // for type:"array"
	Properties  map[string]SchemaProperty `json:"properties,omitempty"` // for type:"object"
	Required    []string                  `json:"required,omitempty"`   // for type:"object"
}

SchemaProperty describes one parameter within an InputSchema. The schema builder helpers (below) populate these fields; Enum supports constrained values; Items, Properties, and Required let arrays and nested objects round-trip without dropping to hand-written JSON.

For schemas richer than these fields express (oneOf, $ref, patternProperties, etc.) construct a Tool directly with a json.RawMessage InputSchema — see the "Hand-rolled schemas" section in RECIPES.md.

func ObjectOf

func ObjectOf(fields ...Property) SchemaProperty

ObjectOf builds a nested-object SchemaProperty, suitable for passing as the item type to Array or as a child of a parent ObjectOf. Use Object at the top level (it returns InputSchema, the type tools require); use ObjectOf anywhere a SchemaProperty is expected.

Example:

Array("subtasks", "List of sub-questions",
    ObjectOf(
        String("question", "Sub-question to research", Required()),
        String("rationale", "Why this matters"),
    ),
    Required())

type Session

type Session struct {
	ID      string                     `json:"id"`
	History []Message                  `json:"history,omitempty"`
	State   map[string]json.RawMessage `json:"state,omitempty"`

	// Events is an append-only activity log populated when a Recorder is
	// attached to the session via RecorderToSession. It records intermediate
	// turn activity — model calls, tool calls, retries — that does not
	// appear in History. Persisted by Store implementations as plain JSON.
	Events []Event `json:"events,omitempty"`
}

Session holds a conversation and optional caller-owned metadata.

A Session is plain data. It does not call models, run tools, trim context, schedule work, or manage application lifecycle. The caller decides when to load a session, pass History to a model call, and persist the result.

State holds arbitrary caller-owned metadata encoded as JSON values. Use the SetState and GetState helpers to read and write typed values:

gocode.SetState(sess, "user_id", "u-123")
gocode.SetState(sess, "turn", 7)

id, _ := gocode.GetState[string](sess, "user_id")
turn, _ := gocode.GetState[int](sess, "turn")

Using json.RawMessage as the value type means State survives JSON round-trips without type loss — MemoryStore and FileStore behave identically for all value types.

Typical usage:

sess, err := store.Get(ctx, id)
if errors.Is(err, gocode.ErrSessionNotFound) {
    sess = &gocode.Session{ID: id}
} else if err != nil {
    return err
}

sess.History = append(sess.History, gocode.NewUserMessage(input))
result, err := assistant.Step(ctx, sess.History)
if err != nil {
    return err
}
sess.History = result.Messages

return gocode.Save(ctx, store, sess) // creates or updates as needed

func Load

func Load(ctx context.Context, store Store, id string) (*Session, error)

Load returns the session with the given ID, or a fresh &Session{ID: id} if no session with that ID exists. It is the read-side symmetry of Save: callers that don't need to distinguish first use from subsequent use can rely on it for the open-or-create pattern. Other errors are returned as-is.

func (*Session) Clone

func (s *Session) Clone() *Session

Clone returns a deep copy of s so neither the caller's original nor the stored copy can alias each other. Store implementations use this on both write and read to guarantee isolation.

type StepFunc

type StepFunc[T any] func(ctx context.Context) (T, error)

StepFunc is a unit of work in a directed workflow. It accepts the shared context and returns a typed result.

type Store

type Store interface {
	// Create persists a new Session. Returns ErrSessionExists if the ID is
	// already present.
	Create(ctx context.Context, session *Session) error

	// Get loads a Session by ID. Returns ErrSessionNotFound if the ID is
	// absent.
	Get(ctx context.Context, id string) (*Session, error)

	// Update overwrites an existing Session. Returns ErrSessionNotFound if
	// the ID is absent.
	Update(ctx context.Context, session *Session) error

	// Delete removes a Session. Returns ErrSessionNotFound if the ID is
	// absent.
	Delete(ctx context.Context, id string) error

	// List returns Sessions whose IDs have the given prefix, up to limit
	// entries sorted by ID. An empty prefix matches all IDs. A limit of 0
	// means no limit.
	List(ctx context.Context, prefix string, limit int) ([]*Session, error)
}

Store persists and retrieves Sessions. All implementations must be safe for concurrent use.

Create and Update are intentionally separate: Create fails if the ID already exists, Update fails if it does not. This makes the caller's intent explicit and avoids silent overwrites. Use Save when you do not need that distinction.

type StreamBuffer

type StreamBuffer struct {
	// contains filtered or unexported fields
}

StreamBuffer provides reset-aware token delivery for streaming calls with retries. When callWithRetry retries a failed stream, the onToken callback may fire again for the new attempt, causing partial output from the failed attempt to appear before the successful attempt's output. StreamBuffer lets callers react to each retry by calling an onReset function, which can clear a display buffer, send an SSE reset event, or take any other corrective action.

Wire StreamBuffer to a streaming call like this:

sb := gocode.NewStreamBuffer(
    func(b gocode.ContentBlock) { fmt.Print(b.Text) }, // forward tokens live
    func() { fmt.Print("\n[retrying…]\n") },          // reset partial output
)
cfg := gocode.RetryConfig{OnRetry: sb.OnRetry}
client, _ := gocode.New(gocode.Config{..., Retry: cfg})
msg, err := client.AskStream(ctx, system, history, sb.OnToken)

Either argument to NewStreamBuffer may be nil, in which case that half of the wiring is silently skipped.

func NewStreamBuffer

func NewStreamBuffer(onToken func(ContentBlock), onReset func()) *StreamBuffer

NewStreamBuffer returns a StreamBuffer that forwards tokens to onToken and calls onReset before each retry attempt. Either argument may be nil.

func (*StreamBuffer) OnRetry

func (b *StreamBuffer) OnRetry(attempt int, wait time.Duration)

OnRetry satisfies the RetryConfig.OnRetry signature. It calls the onReset handler so callers can clear any partial output before the next stream attempt begins. attempt is the 1-based retry number; wait is the computed backoff duration.

func (*StreamBuffer) OnToken

func (b *StreamBuffer) OnToken(cb ContentBlock)

OnToken is the onToken callback to pass to AskStream, LoopStream, or StepStream. It forwards each ContentBlock to the underlying onToken handler.

type Tool

type Tool struct {
	Name        string          `json:"name"`
	Description string          `json:"description,omitempty"`
	InputSchema json.RawMessage `json:"input_schema,omitempty"`

	// Provider, if set, restricts this tool to a specific provider tag
	// (e.g. "anthropic"). Providers reject mismatched tools at request
	// build time so misuse fails loudly rather than silently dropping the
	// declaration.
	Provider string `json:"-"`

	// CacheControl, if set, marks this tool as a cache breakpoint in the
	// request. Caching is cumulative; placing the marker on the last tool
	// in a stable toolset caches the system prompt and all earlier tools.
	// Currently honored by AnthropicProvider and OpenRouterProvider.
	// Ignored when Raw is set (the verbatim JSON wins).
	CacheControl *CacheControl `json:"cache_control,omitempty"`

	// Raw, if set, replaces the wire serialization of this tool. Used by
	// provider-defined tools whose declaration form is not the standard
	// {name, description, input_schema}. The agent loop never inspects Raw.
	Raw json.RawMessage `json:"-"`
}

Tool defines a capability available to the agent during a Loop. InputSchema is stored as pre-serialized JSON so callers can supply schemas richer than InputSchema expresses (nested objects, arrays, $defs) without the library needing to understand them.

For provider-defined client-executed tools (category 2 — e.g. Anthropic's bash_20250124, text_editor_20250124, computer_20250124) the wire shape is {"type": "...", "name": "..."} rather than {name, description, input_schema}. Such tools set Provider and Raw: Provider tags the binding to one provider, and Raw is the verbatim JSON declaration spliced into the wire tools array. Name remains populated so the agent loop can dispatch tool_use blocks returned by the model.

func NewTool

func NewTool(name, description string, schema InputSchema) Tool

NewTool constructs a Tool from a typed InputSchema.

Panics if schema cannot be marshalled to JSON. InputSchema is a plain Go struct of strings, maps, and slices — marshal cannot fail in practice, so the panic is a programmer-error indicator (corrupt unsafe.Pointer, etc.) rather than a runtime condition callers should handle.

func (Tool) MarshalJSON

func (t Tool) MarshalJSON() ([]byte, error)

MarshalJSON emits Raw verbatim when set (category-2 provider-defined tools have a non-standard wire form). Otherwise it emits the standard {name, description, input_schema} object.

type ToolBinding

type ToolBinding struct {
	Tool Tool
	Func ToolFunc
	Meta ToolMetadata
}

ToolBinding pairs a Tool definition with its ToolFunc implementation and optional advisory metadata. It is the unit of composition for Toolset.

func Bind

func Bind(t Tool, fn ToolFunc) ToolBinding

Bind is the one-line constructor for a ToolBinding with no metadata. Equivalent to ToolBinding{Tool: t, Func: fn}.

type ToolError

type ToolError struct {
	ToolName  string
	ToolUseID string
	Cause     error
}

ToolError is returned when a dispatch lookup fails (tool missing from map). Individual tool execution errors are soft-faulted into is_error results and are never wrapped in ToolError.

func (*ToolError) Error

func (e *ToolError) Error() string

func (*ToolError) Unwrap

func (e *ToolError) Unwrap() error

type ToolFunc

type ToolFunc func(ctx context.Context, input json.RawMessage) (string, error)

ToolFunc is the signature every tool implementation must satisfy. input is the raw JSON the model produced as arguments for this call. Return the result as a plain string, or an error if the tool failed. Errors are fed back to the model as is_error=true results so it can recover; they do not abort the loop.

func TypedToolFunc

func TypedToolFunc[Input any](f func(context.Context, Input) (string, error)) ToolFunc

TypedToolFunc returns a ToolFunc that automatically unmarshals the raw JSON input into the generic Input type before calling the provided handler. This removes the repetitive json.Unmarshal + error handling boilerplate while producing an ordinary ToolFunc that works with any dispatch map and preserves all existing error/inspectability behavior.

For struct Input types, empty input (nil, zero-length, or JSON "null") is treated as `{}` to avoid unmarshal errors on tools with no params. Other types (string, int, etc.) unmarshal strictly.

See NewTypedTool(...) for a one-line way to obtain both a Tool and the corresponding ToolFunc. Example (from the roadmap):

type CalculatorInput struct {
	Operation string  `json:"operation"`
	A         float64 `json:"a"`
	B         float64 `json:"b"`
}

fn := TypedToolFunc(func(ctx context.Context, in CalculatorInput) (string, error) {
	switch in.Operation {
	case "add":
		return fmt.Sprintf("%f", in.A+in.B), nil
	case "subtract":
		return fmt.Sprintf("%f", in.A-in.B), nil
	case "multiply":
		return fmt.Sprintf("%f", in.A*in.B), nil
	default:
		return "", fmt.Errorf("unknown operation: %s", in.Operation)
	}
})

The resulting fn can be used directly in a dispatch map[string]ToolFunc.

type ToolMetadata

type ToolMetadata struct {
	Source               string
	ReadOnly             bool
	Destructive          bool
	Network              bool
	Filesystem           bool
	Shell                bool
	RequiresConfirmation bool
	SafetyNotes          []string
	Terminal             bool
}

ToolMetadata carries advisory annotations about a bound tool.

Most fields are informational only; the library does not enforce policy based on them. Applications may inspect them to build confirmation wrappers, audit logs, or permission checks.

Terminal is the one field with semantic effect: when a tool with Meta.Terminal == true is invoked successfully (no IsError result), Loop and LoopStream return after appending the tool result message, without asking the model for a final text turn. This is the primitive behind Extract — it lets a "submit_X" style tool double as the loop's exit signal.

type ToolResult

type ToolResult struct {
	ToolUseID string // matches ContentBlock.ID from the corresponding tool_use block
	Content   string // the string returned by ToolFunc, or the error message
	IsError   bool

	// Images, if non-empty, are image attachments produced by the tool.
	// The agent loop populates this from any AttachImage calls a tool
	// made during its run. NewToolResultMessage flattens them onto the
	// canonical user-role message; the OpenAI wire serializer emits them
	// as image_url parts on a sibling user message.
	Images []ImageBlock
}

ToolResult is the output of one ToolFunc execution.

type ToolUse

type ToolUse struct {
	ID    string
	Name  string
	Input json.RawMessage
}

ToolUse is extracted from an assistant ContentBlock and passed to dispatch.

type Toolset

type Toolset struct {
	Bindings      []ToolBinding
	ProviderTools []ProviderTool
}

Toolset is an ordered collection of ToolBindings. It is the input shape for Client.Loop and Client.LoopStream. Tools() and Dispatch() expose the raw slice and map for callers that want to inspect or use them directly.

ProviderTools holds server-executed (category-1) tools advertised to the provider — e.g. Anthropic's web_search, code_execution. They have no Go implementation; the provider runs them and returns inline result blocks. The agent loop never inspects them: Tools() and Dispatch() ignore the slot and only describe local Bindings. Use the typed provider constructors (AnthropicWebSearch, etc.) and attach them via WithProviderTools or by setting the field directly.

func Join

func Join(sets ...Toolset) (Toolset, error)

Join merges multiple Toolset values into one, preserving order. Returns an error if any tool name appears more than once across the sets. ProviderTools are concatenated without deduplication (each entry is opaque JSON; collision is ill-defined).

func MustJoin

func MustJoin(sets ...Toolset) Toolset

MustJoin is like Join but panics on duplicate tool names. It is intended for static composition of toolsets at program startup, where a duplicate is a programmer error rather than a runtime condition. Follows the regexp.MustCompile / template.Must convention.

func Tools

func Tools(bindings ...ToolBinding) Toolset

Tools is a variadic constructor for a Toolset. It is the most ergonomic way to build a small toolset literally:

tools := gocode.Tools(
    gocode.Bind(searchTool, searchFn),
    gocode.Bind(submitTool, submitFn),
)

Equivalent to Toolset{Bindings: []ToolBinding{...}}.

func (Toolset) CacheLast

func (t Toolset) CacheLast(cache *CacheControl) Toolset

CacheLast returns a copy of t with the last tool binding marked as a cache breakpoint. Anthropic cache markers are cumulative — a marker on the last tool caches the system prompt and every preceding tool — so this is the standard "cache the stable prefix" pattern.

No-op for empty toolsets. Honored only by providers that support cache markers (AnthropicProvider, OpenRouterProvider routing to Anthropic); other providers ignore it.

func (Toolset) Dispatch

func (t Toolset) Dispatch() map[string]ToolFunc

Dispatch returns the name→func map derived from the bindings.

func (Toolset) Tools

func (t Toolset) Tools() []Tool

Tools returns the Tool slice — the model-facing definitions — derived from the bindings. Useful for inspection or for callers building their own loop on top of the primitive provider interfaces.

func (Toolset) WithProviderTools

func (t Toolset) WithProviderTools(pt ...ProviderTool) Toolset

WithProviderTools returns a copy of t with the supplied ProviderTools appended to the existing slot. Useful for fluent composition:

tools := agent.Tools(localBinding).
    WithProviderTools(agent.AnthropicWebSearch(agent.WebSearchOpts{MaxUses: 5}))

func (Toolset) Wrap

func (t Toolset) Wrap(middlewares ...Middleware) Toolset

Wrap applies each Middleware to every binding in the Toolset, returning a new Toolset with the decorated functions. Middlewares are applied in order, so the first one listed is outermost (executes first and last). ProviderTools (category-1) carry through unchanged — middleware applies only to local Bindings.

type Usage

type Usage struct {
	InputTokens         int `json:"input_tokens"`
	OutputTokens        int `json:"output_tokens"`
	CacheCreationTokens int `json:"cache_creation_input_tokens,omitempty"`
	CacheReadTokens     int `json:"cache_read_input_tokens,omitempty"`
}

Usage records token consumption for one API call.

CacheCreationTokens and CacheReadTokens carry prompt-cache statistics from providers that report them (Anthropic and OpenRouter today). CacheCreationTokens are billed input tokens that wrote a fresh cache entry; CacheReadTokens are input tokens served from cache at a discount. Providers that don't surface cache info leave both fields zero.

Directories

Path Synopsis
cmd
gocode command
gocode is a CLI coding agent built on the gocode toolkit.
gocode is a CLI coding agent built on the gocode toolkit.
examples
agent command
Tier 3 example: a full agentic loop with tool use.
Tier 3 example: a full agentic loop with tool use.
ask command
Tier 1 example: a single LLM call.
Tier 1 example: a single LLM call.
pipeline command
Tier 2 example: parallel steps feeding a sequential step.
Tier 2 example: parallel steps feeding a sequential step.
recipes/01-minimal command
Recipe 01-minimal: the smallest tool-using agent gocode can express with primitives alone.
Recipe 01-minimal: the smallest tool-using agent gocode can express with primitives alone.
recipes/02-agent-with-tools command
Recipe 01: a single Agent with a curated toolset and streaming output.
Recipe 01: a single Agent with a curated toolset and streaming output.
recipes/03-repo-explainer command
Recipe 02: repo-explainer.
Recipe 02: repo-explainer.
recipes/04-router-subagents command
Recipe 04: a router orchestrator that delegates to specialist subagents.
Recipe 04: a router orchestrator that delegates to specialist subagents.
recipes/05-persistent-chat command
Recipe 05: persistent chat with a per-turn activity log.
Recipe 05: persistent chat with a per-turn activity log.
recipes/06-parallel-pipeline command
Recipe 06: parallel steps feeding a sequential step.
Recipe 06: parallel steps feeding a sequential step.
recipes/07-web-service command
Recipe 07-web-service: the smallest deploy-shaped HTTP server that fronts a gocode Agent.
Recipe 07-web-service: the smallest deploy-shaped HTTP server that fronts a gocode Agent.
stream command
Streaming demo: live CLI output using AskStream (and LoopStream is similar).
Streaming demo: live CLI output using AskStream (and LoopStream is similar).
Package mcp provides a client for the Model Context Protocol (MCP).
Package mcp provides a client for the Model Context Protocol (MCP).
providers
tools
bash
Package bash provides a sandboxed shell tool for the coding agent.
Package bash provides a sandboxed shell tool for the coding agent.
batch
Package batch provides a single tool, "batch", that runs N other tool calls concurrently in one model turn.
Package batch provides a single tool, "batch", that runs N other tool calls concurrently in one model turn.
clock
Package clock provides a safe, read-only tool that returns the current UTC time.
Package clock provides a safe, read-only tool that returns the current UTC time.
editor
Package editor implements the handlers for Anthropic's str_replace_based_edit_tool (text_editor_20250728) sandboxed to a workspace root.
Package editor implements the handlers for Anthropic's str_replace_based_edit_tool (text_editor_20250728) sandboxed to a workspace root.
math
Package math provides a safe calculator tool for basic arithmetic.
Package math provides a safe calculator tool for basic arithmetic.
subagent
Package subagent packages an Agent (client + system prompt + toolset) as a single tool the parent agent can call.
Package subagent packages an Agent (client + system prompt + toolset) as a single tool the parent agent can call.
todo
Package todo provides an in-memory todo list as a pair of tools.
Package todo provides an in-memory todo list as a pair of tools.
web
Package web implements a stdlib-only web_fetch tool: download a URL, convert HTML to plain text, and return a slice of the result with optional pagination.
Package web implements a stdlib-only web_fetch tool: download a URL, convert HTML to plain text, and return a slice of the result with optional pagination.
workspace
Package workspace provides a safe, sandboxed set of filesystem tools rooted at a configurable directory.
Package workspace provides a safe, sandboxed set of filesystem tools rooted at a configurable directory.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL