Documentation
¶
Overview ¶
Package agent is a durability-first, Go-idiomatic toolkit for building AI agents.
The agent loop journals every step to a Durable store via named-step memoization (Durable.Do) and resumes after a crash — reusing recorded steps, re-running retry-safe tools, halting (rather than double-executing) a non-idempotent tool whose outcome is unknown, and pausing durably for human approval when a tool requires it. Orchestration is plain Go (Option B); the model call is wrapped by a func(Handler) Handler middleware chain. Reliability wrappers ship in the middleware package: per-attempt timeouts, classified retry with backoff, hedged model calls (middleware.Hedge), and rate limiting.
Scope: this resumes AROUND tool boundaries, not the internals of a single in-flight tool call. The precise promise is "survives crashes around tool calls, never double-fires an unsafe side effect" — not "mid-tool-call resume".
Index ¶
- Constants
- Variables
- func Ack(ctx context.Context, d Durable, runID, channel, key string) error
- func ApprovalDecisionBytes(s ApprovalSubject, approverID string, approved bool) []byte
- func ApprovalTallyStep(toolUseID string) string
- func Approve(ctx context.Context, d Durable, runID, toolUseID string, approved bool) error
- func ApproveAs(ctx context.Context, d Durable, runID, toolUseID, approverID string, ...) error
- func Await[T any](ctx context.Context, name string) (T, error)
- func AwaitFor[T any](ctx context.Context, name string, d time.Duration) (T, bool, error)
- func ClassifyHTTPError(provider string, resp *http.Response) error
- func DetachModelSink(ctx context.Context) (context.Context, func(Event))
- func EmitMessage(sink func(Event), m Message)
- func EncodeToolResultOr(c ToolResultCodec, raw json.RawMessage) string
- func Generate(ctx context.Context, m Model, req Request) (Message, Usage, error)
- func InSaga(ctx context.Context) bool
- func Interrupt[T any](ctx context.Context, key string, prompt any) (T, error)
- func IsApprovalDecision(r Record, toolUseID string) bool
- func IsComplete(ctx context.Context, store Durable, runID string) (bool, error)
- func Lease(ctx context.Context, store Durable, runID string, ...) (bool, error)
- func NewSSEScanner(body io.Reader) *bufio.Scanner
- func Parallel[T any](ctx context.Context, d Durable, runID string, maxConcurrency int, ...) ([]T, error)
- func ParseRetryAfter(s string) time.Duration
- func Recover(ctx context.Context, store Durable, ...) (int, error)
- func RenderMermaid(ctx context.Context, d Durable, runID string) (string, error)
- func ResolveHalt(ctx context.Context, store Durable, runID, toolUseID string, result any, ...) error
- func Resume[T any](ctx context.Context, d Durable, runID, key string, value T) error
- func RunScope(ctx context.Context) string
- func RunTyped[T any](ctx context.Context, a *Agent, runID, input string) (T, error)
- func RunTypedNative[T any](ctx context.Context, a *Agent, runID, input string) (T, error)
- func SSEPayload(line string) (string, bool)
- func Send[T any](ctx context.Context, d Durable, runID, channel, key string, payload T) error
- func Signal[T any](ctx context.Context, d Durable, runID, name string, payload T) error
- func Sleep(ctx context.Context, name string, d time.Duration) error
- func Step[T any](ctx context.Context, d Durable, runID, name string, ...) (T, error)
- func TallyApprovals(recs []Record, s ApprovalSubject, p ApprovalPolicy, ...) (ApprovalTally, []DecisionCheck)
- func WaitUntil(ctx context.Context, name string, until time.Time) error
- func WithClock(ctx context.Context, now func() time.Time) context.Context
- func WithIdentity(ctx context.Context, id Identity) context.Context
- func WithWaker(ctx context.Context, w Waker) context.Context
- type APIError
- type Agent
- func (a *Agent) Run(ctx context.Context, runID, input string) (Message, error)
- func (a *Agent) RunResult(ctx context.Context, runID, input string) (*Result, error)
- func (a *Agent) RunSaga(ctx context.Context, runID, input string) (Message, error)
- func (a *Agent) RunSagaResult(ctx context.Context, runID, input string) (*Result, error)
- func (a *Agent) Session(ctx context.Context, id string) (*Session, error)
- func (a *Agent) SetMaxConcurrency(n int) *Agent
- func (a *Agent) Stream(ctx context.Context, runID, input string) *AgentStream
- func (a *Agent) StreamSaga(ctx context.Context, runID, input string) *AgentStream
- func (a *Agent) Use(mw ...Middleware) *Agent
- func (a *Agent) UseTool(mw ...ToolMiddleware) *Agent
- func (a *Agent) WithApproverVerifiers(fn ApproverVerifierFor) *Agent
- func (a *Agent) WithMaxTurns(n int) *Agent
- func (a *Agent) WithSampling(opts ...SamplingOption) *Agent
- func (a *Agent) WithSystemPrompt(s string) *Agent
- func (a *Agent) WithSystemPromptFunc(fn func(context.Context) string) *Agent
- func (a *Agent) WithToolChoice(tc ToolChoice) *Agent
- type AgentEvent
- type AgentStream
- type ApprovalPolicy
- type ApprovalRequired
- type ApprovalSubject
- type ApprovalTally
- type ApproveOption
- type ApproverVerifier
- type ApproverVerifierFor
- type AssistantTurn
- type Awaiting
- type Compensator
- type DecisionCheck
- type Doc
- type Durable
- type Emit
- type Event
- type Finish
- type Finished
- type HaltTooYoung
- type Identity
- type Image
- type Interrupted
- type JSONToolResultCodec
- type Leaser
- type Lister
- type MemStore
- func (m *MemStore) AcquireLease(_ context.Context, runID, holder string, ttl time.Duration) (bool, error)
- func (m *MemStore) Do(ctx context.Context, runID, name string, ...) (Record, error)
- func (m *MemStore) History(_ context.Context, runID string) ([]Record, error)
- func (m *MemStore) ReleaseLease(_ context.Context, runID, holder string) error
- func (m *MemStore) RenewLease(_ context.Context, runID, holder string, ttl time.Duration) (bool, error)
- func (m *MemStore) Runs(_ context.Context) ([]string, error)
- type MemWaker
- type Message
- type Middleware
- type Model
- type ModelEvent
- type ModelHandler
- type Part
- type PendingApproval
- type RateLimited
- type Reasoning
- type ReasoningDelta
- type Received
- type Record
- type RecoverOption
- type Request
- type ResolveOption
- type ResponseFormat
- type Result
- type ResumeHalt
- type Retriever
- type Role
- type Safety
- type SagaAborted
- type Sampling
- type SamplingOption
- type ScriptedModel
- type ScriptedTurn
- type Session
- type Sleeping
- type StepKind
- type Stream
- type Task
- type Text
- type TextDelta
- type Tool
- type ToolCallDelta
- type ToolChoice
- type ToolCompleted
- type ToolHandler
- type ToolMiddleware
- type ToolResult
- type ToolResultCodec
- type ToolStarted
- type ToolUse
- type TurnStarted
- type Usage
- type Waker
Examples ¶
Constants ¶
const ( ReasonNotEligible = "not an eligible approver" ReasonSuperseded = "superseded by the approver's earlier valid decision" ReasonNoKey = "no key for this approver" ReasonBadSig = "signature does not verify for this call" )
Reasons a decision record does not count.
Variables ¶
var ( // ErrConfig is a programmer-facing misuse or misconfiguration, not a runtime fault. ErrConfig = errors.New("config") // ErrModel is a model-provider call that failed: HTTP status, decode, stream error. ErrModel = errors.New("model") // ErrTool is a tool that failed or was misused (unknown tool, bad arguments). ErrTool = errors.New("tool") // ErrStorage is a durable-store I/O failure: marshal, insert, query, history. ErrStorage = errors.New("storage") // ErrProtocol is malformed wire or journal data (bad log entry, unknown content part). ErrProtocol = errors.New("protocol") // ErrBudget is a middleware limit tripping (e.g. a token budget). ErrBudget = errors.New("budget") )
The toolkit classifies failures with sentinel errors matched by errors.Is — the standard-library idiom (io.EOF, sql.ErrNoRows). There are two tiers:
- Category sentinels (ErrModel, ErrTool, ErrStorage, ErrConfig, ErrProtocol, ErrBudget) — the coarse class of failure, for a broad branch or a metric label.
- Condition sentinels (ErrUnknownTool, ErrToolArgs, …) — a specific cause. Each wraps its category, so errors.Is(err, ErrUnknownTool) and errors.Is(err, ErrTool) both hold.
Every error the toolkit returns wraps exactly one category (and usually the underlying cause too, via a second %w). Match the class you care about:
if errors.Is(err, agent.ErrModel) { retryLater() } // any model-provider fault
if errors.Is(err, agent.ErrUnknownTool) { … } // one specific condition
The control-flow signals are richer than a category, so they stay concrete types matched with errors.As, not sentinels: *PendingApproval (human approval needed), *ResumeHalt (unsafe to resume), *SagaAborted (transaction rolled back). context cancellation surfaces as the usual context.Canceled / context.DeadlineExceeded.
var ( ErrUnknownTool = fmt.Errorf("unknown tool: %w", ErrTool) ErrToolArgs = fmt.Errorf("invalid tool arguments: %w", ErrTool) ErrNoRecordedOutput = fmt.Errorf("no recorded model output: %w", ErrModel) ErrTruncatedToolArgs = fmt.Errorf("truncated tool-call arguments: %w", ErrProtocol) ErrBudgetExceeded = fmt.Errorf("budget exceeded: %w", ErrBudget) ErrMaxTurns = fmt.Errorf("max turns exceeded: %w", ErrBudget) // ErrInvalidApproval is an approver decision rejected at submission by ApproveAs's // WithDecisionCheck: no such tool call, an unknown approver, or a signature that does not // verify for this exact call. ErrInvalidApproval = fmt.Errorf("invalid approval: %w", ErrConfig) // ErrAlreadyDecided is a decision submitted (with WithDecisionCheck) by an approver whose // earlier valid decision on the same call already counts; the new one would be ignored. ErrAlreadyDecided = fmt.Errorf("approver already decided: %w", ErrConfig) )
Condition sentinels — specific causes, each wrapping its category.
Functions ¶
func Ack ¶
Ack marks a message consumed so Receive advances past it. Idempotent (the first ack for a (runID, channel, key) wins) and journaled, so the advance survives a crash and a resume.
func ApprovalDecisionBytes ¶ added in v0.4.0
func ApprovalDecisionBytes(s ApprovalSubject, approverID string, approved bool) []byte
ApprovalDecisionBytes returns the canonical bytes an approver signs and every verifier checks. The layout, stable and reproducible outside Go:
"bide.approval.v2\n" len32(RunID) RunID len32(ToolUseID) ToolUseID len32(ToolName) ToolName SHA-256(canonical args), 32 bytes len32(approverID) approverID 1 byte: 1 approved, 0 denied
len32 is a 4-byte big-endian length, so no two distinct decisions encode to the same bytes. The canonical args are the arguments parsed as JSON and re-serialized with object keys sorted, no insignificant whitespace, no HTML escaping, and number literals kept verbatim; empty arguments are "{}", and bytes that are not a single JSON value are used as-is. The canonical form makes the signature independent of how the arguments were formatted in transit (a live model stream, the journal, an evidence file).
func ApprovalTallyStep ¶ added in v0.4.0
ApprovalTallyStep is the journal name of the gate's terminal tally for toolUseID.
func Approve ¶
Approve durably records a human approve/deny decision for a tool call (HITL). It is idempotent: the first decision for a (runID, toolUseID) wins. After approving, re-run the agent with the same runID and it resumes past the pending-approval pause. The decision survives a crash because it's a journaled step like any other.
func ApproveAs ¶ added in v0.4.0
func ApproveAs(ctx context.Context, d Durable, runID, toolUseID, approverID string, approved bool, sig []byte, opts ...ApproveOption) error
ApproveAs records one named approver's signed decision on a tool call gated by an m-of-n Approval policy. sig is the approver's signature over ApprovalDecisionBytes(pend.Subject(), approverID, approved). After enough decisions land, re-run with the same runID. (A single-approver gate uses Approve.)
Each distinct decision is its own journal record; resubmitting the identical decision is a no-op. The gate counts each approver's first valid decision and ignores the rest, so an invalid record never blocks a valid one. Without WithDecisionCheck, ApproveAs does not verify: a decision that will not count is recorded and reported by the gate as ignored.
func Await ¶
Await blocks the current run until a single-shot signal named `name` is delivered, then returns its payload. Call it from inside a tool (the agent loop supplies the run context). On first encounter, with no signal recorded, it returns the zero T and an *Awaiting error that propagates out of Run, pausing the run durably. After Signal records a payload for the same name and Run is re-invoked, Await returns that payload and execution continues past this point. The payload survives a crash: it is a journaled step.
Await is the externally-pushed counterpart of Interrupt. Like Interrupt and Sleep it must be called from a retry-safe tool (Safety.ReadOnly or Idempotent): on resume the tool re-runs from the top until the await resolves, so everything before the Await call must be safe to repeat. Use distinct names for distinct awaits; each pauses and resolves independently.
func AwaitFor ¶
AwaitFor blocks the current run until either a single-shot signal named `name` is delivered or the duration d elapses, whichever happens first. It returns (payload, true, nil) when the signal wins, or (zero, false, nil) when the timeout wins. Call it from inside a retry-safe tool (Safety.ReadOnly or Idempotent), like Await and Sleep: on resume the tool re-runs from the top until the await resolves, so everything before the AwaitFor call must be safe to repeat.
It is Await composed with a durable timer. The deadline is journaled once, on the first encounter (at-most-once by name), as now()+d, so a resumed or crash-recovered run races against the same absolute instant rather than restarting the clock. While neither side has resolved, AwaitFor schedules a wake with the bound Waker (if any) and returns *Awaiting, pausing the run durably exactly like Await. The bool return is only meaningful when err is nil.
func ClassifyHTTPError ¶
ClassifyHTTPError turns a non-2xx model response into the appropriate SDK error: a *RateLimited (with the Retry-After hint) for HTTP 429, or a *APIError carrying the status code and a body snippet otherwise. It reads and closes resp.Body, so call it only on a response the adapter is abandoning (not one it will go on to stream). provider names the adapter for the wrapped message.
func DetachModelSink ¶ added in v0.5.0
DetachModelSink returns ctx with the run's token sink removed, and that sink (nil when the run is not streaming). Agent.Stream installs the sink so a model's token deltas reach the caller as they arrive. Middleware that sends one model call to several targets must detach it before calling them: otherwise a target that loses streams text the run never records, and may keep sending after the run has ended. Once it has chosen the response to return, the middleware can deliver it to the caller with EmitMessage.
func EmitMessage ¶ added in v0.5.0
EmitMessage delivers m to sink as the events a model would have streamed for it. It does nothing when sink is nil. See DetachModelSink.
func EncodeToolResultOr ¶ added in v0.2.0
func EncodeToolResultOr(c ToolResultCodec, raw json.RawMessage) string
EncodeToolResultOr applies the codec to raw and returns the result. When the codec is nil it returns the canonical JSON string (the default behavior). When the codec returns an error it degrades to the same JSON string rather than failing the whole model request: the JSON form is always a correct payload, so a codec hiccup never drops a tool result. Adapters call this at the point where a ToolResult is serialized into a provider request.
func InSaga ¶
InSaga reports whether the current tool execution is inside a saga run. Sub-agents use it to run transactionally (RunSaga) so a failure deep in the tree aborts the whole tree.
func Interrupt ¶
Interrupt pauses the current run to request typed human input, identified by key. Call it from inside a tool (the agent loop supplies the run context). On first encounter it returns the zero T and an *Interrupted error that propagates out of Run, pausing the run durably. After Resume records a value for the same key and Run is re-invoked, Interrupt returns that value and execution continues past this point. This generalizes approve/deny (a bool) to an arbitrary typed answer.
Interrupt must be called from a retry-safe tool (Safety.ReadOnly or Idempotent): on resume the tool re-runs from the top until the interrupt resolves, so everything before the Interrupt call must be safe to repeat. Use distinct keys for multiple interrupt points; each pauses and resumes independently.
func IsApprovalDecision ¶ added in v0.4.0
IsApprovalDecision reports whether r is an m-of-n approver decision (written by ApproveAs) on toolUseID. The single-approver Approve record has no Approver and is not one.
func IsComplete ¶
IsComplete reports whether runID has reached its terminal answer, by checking the journal for the durable completion marker the agent loop records at the end of a run (see runCompleteStep). A crash-recovery supervisor uses it to skip finished runs.
func Lease ¶
func Lease(ctx context.Context, store Durable, runID string, drive func(context.Context) error, opts ...RecoverOption) (bool, error)
Lease runs drive under an exclusive, auto-renewed lease on runID, so a primary driver and a recoverer (or two workers) do not drive the same run at once. If the store implements Leaser and another holder currently leases the run, drive is NOT called and Lease returns (false, nil). If the store does not implement Leaser, drive runs unconditionally. The bool reports whether drive ran; the error is the acquisition error (when false) or drive's own error (when true).
A primary driver wraps Agent.Run so a live run and a recoverer never both drive it, using the same lease the recoverer respects:
driven, err := agent.Lease(ctx, store, runID, func(ctx context.Context) error {
_, err := ag.Run(ctx, runID, input)
return err
}, agent.WithLeaseHolder("worker-1"))
func NewSSEScanner ¶
NewSSEScanner returns a bufio.Scanner over an SSE stream body with the default 64KB line cap raised to 1MB. A single SSE line can carry a large tool-call arguments blob, and the 64KB default would silently truncate it and desync the stream (the openai-go #368 lesson). Every adapter reads its event stream through this so the cap is fixed in exactly one place.
func Parallel ¶
func Parallel[T any](ctx context.Context, d Durable, runID string, maxConcurrency int, tasks ...Task[T]) ([]T, error)
Parallel runs tasks concurrently, each as a durable named step (see Step), and returns their results in task order. It is the durable, auditable fan-in that compliance-style pipelines want: run several independent checks at once (sanctions, credit, fraud), each one crash-safe and at-most-once, each result committed to the journal and provable on its own, then aggregate.
All tasks run even if some fail, so the full set of results is preserved (a failed check does not hide the others). The returned error joins every task's error and is nil only if all succeeded; a task that failed was not journaled, so on a later resume it re-runs while succeeded tasks are memoized. maxConcurrency caps in-flight tasks; <= 0 means one goroutine per task.
This is deliberately a thin primitive over the durable journal, not a graph engine: dynamic, model-driven routing stays in plain Go and sub-agents; Parallel covers the static fan-out/fan-in that a governed workflow's parallel-checks-then-decide stage is made of.
As with Step, each task result is journaled as JSON, so T must be JSON-serializable; a struct's unexported fields silently round-trip to their zero values.
func ParseRetryAfter ¶
ParseRetryAfter parses an HTTP Retry-After header value. It accepts either an integer number of seconds or an HTTP-date, and returns 0 if the value is absent, unparseable, or already in the past.
func Recover ¶
func Recover(ctx context.Context, store Durable, resume func(ctx context.Context, runID string) error, opts ...RecoverOption) (int, error)
Recover re-drives the runs that were in flight when the process died. It enumerates every run the store holds (via Lister), skips the ones already marked complete, and calls resume for each remaining run to push it forward. It returns how many runs it re-drove and the joined genuine failures (nil if none).
The store must implement Lister; a store that cannot enumerate its runs (the base Durable contract does not require it) yields an ErrConfig-wrapped error.
If the store also implements Leaser, Recover coordinates across processes: it claims an exclusive, renewed lease per run before driving it and skips a run another holder currently leases, so competing recoverers do not both re-drive the same run. A crash lets the lease expire (see WithLeaseTTL) and another process takes over. Without Leaser, Recover drives every enumerated run, which is safe under at-most-once memoization but redundant across processes.
A pause is a SUCCESS, not a failure. A re-driven run that is still waiting returns one of the durable pause signals (*PendingApproval, *Interrupted, *Sleeping, *Awaiting, *ResumeHalt); Recover detects those with errors.As and does NOT record them as errors: they mean "recovered, still waiting", and the run resumes later when its condition is met (a human approves, an interrupt is answered, a timer fires). Only a genuine error (a model or storage fault, a bad tool) is joined into the returned error.
resume is deployment POLICY, not a mechanism the SDK can supply: it alone knows a run's original input and any Waker or clock to bind onto the context (a Waker-bound resume rebuilds the timer set for sleeping runs, since Sleep re-registers its wake on replay). A typical resume is:
func(ctx context.Context, runID string) error {
_, err := agent.Run(agent.WithWaker(ctx, w), runID, inputFor(runID))
return err
}
resume should no-op a runID it does not own: a sub-agent run is driven by its parent, so re-driving one directly is redundant (harmless under at-most-once memoization, but the parent already replays it). Recover re-drives every incomplete run it enumerates; let resume decide which ones it is responsible for.
func RenderMermaid ¶
RenderMermaid returns a Mermaid flowchart of a run's journaled steps — the "graph as derived OUTPUT" (Option B): you write plain Go, and the graph is rendered from what actually ran, not hand-authored. Feed it to the dev UI, a trace viewer, or a PR.
func ResolveHalt ¶
func ResolveHalt(ctx context.Context, store Durable, runID, toolUseID string, result any, isError bool, opts ...ResolveOption) error
ResolveHalt is the sanctioned escape from a ResumeHalt. After a non-retriable tool halted with an unknown outcome (see ResumeHalt), an operator who has verified the real side effect out of band injects the missing tool result directly, keyed by the halted tool-use ID (the same key the agent loop uses), so a re-run proceeds past the halt instead of halting again. Pass the result value (JSON-marshalled here) an actual call would have returned, and isError if the verified outcome was a failure the model should react to.
It is idempotent: the first result for a (runID, toolUseID) wins, so calling it twice or racing a concurrent driver injects the record at most once. runID and toolUseID come straight off the ResumeHalt. After resolving, re-run the agent with the same runID:
var halt *agent.ResumeHalt
if errors.As(err, &halt) {
// operator confirms out of band that the charge did go through
_ = agent.ResolveHalt(ctx, store, halt.RunID, halt.ToolUseID, "charged (operator-confirmed)", false)
msg, err = a.Run(ctx, halt.RunID, input) // resumes past the halt
}
Two options refine this. WithMinHaltAge(d) refuses to resolve a halt younger than d (measured from the attempt marker), so a reconciler cannot query and resolve before the provider's record has settled and thereby re-fire the effect. WithEvidence(v) records the resolution as reconciled and stores what was read to decide, signed beside the outcome, so a clean run stays distinguishable from a reconciled one.
This is the only supported way to clear a ResumeHalt for a non-idempotent side effect; deciding the true outcome is a human (or reconciler) judgment the runtime cannot make for you.
func Resume ¶
Resume records the typed value a paused run is waiting for at key (see Interrupt), then re-invoke Run with the same runID to continue. Idempotent: the first value for a (runID, key) wins. The value survives a crash: it is a journaled step.
func RunScope ¶
RunScope returns the hierarchical run scope for the current tool execution (parentRunID/toolUseID). Sub-agents use it to derive a stable, resumable sub-run ID.
func RunTyped ¶
RunTyped runs the agent to completion like Run, but returns a typed result T instead of a free-form Message. It injects a synthetic final_answer tool whose JSON schema is derived from T (via the schema package) and instructs the model to call it once, with the structured answer, when its work is done. Real tools still run first, so a tool-using agent can do work and then answer typed.
The value is decoded from the JOURNALED tool-call args, not captured live, so it is resume-safe: on resume the answer is recovered from the log without re-running anything. If the model answers in plain text instead of calling the tool, RunTyped falls back to parsing that text as T.
T is intended to be a struct (the usual structured-output shape). Because Go methods cannot add type parameters, this is a package function: agent.RunTyped[MyResult](ctx, a, id, in).
Example ¶
ExampleRunTyped shows structured output: RunTyped returns a typed value, decoded from a schema-guided final_answer tool call (a real adapter streams the call the same way).
type Weather struct {
City string `json:"city"`
TempF int `json:"temp_f"`
}
m := &scriptModel{turns: [][]Emit{
toolTurn("c1", "final_answer", `{"city":"SF","temp_f":68}`),
textTurn("done"),
}}
a := New(m, NewMemStore())
w, err := RunTyped[Weather](context.Background(), a, "run-1", "weather in SF?")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%s is %d°F\n", w.City, w.TempF)
Output: SF is 68°F
func RunTypedNative ¶
RunTypedNative is like RunTyped but uses the provider's NATIVE structured-output constraint (a JSON-schema response format) instead of the injected final_answer tool: it sets Request.ResponseFormat from T's schema and decodes the model's direct JSON output. Prefer it on OpenAI-compatible providers with strict structured outputs (schema adherence is enforced provider-side, no tool round-trip). Providers that don't support response formats (e.g. Anthropic) ignore the constraint — use the provider-agnostic RunTyped there. Package function (Go methods can't add type parameters).
func SSEPayload ¶
SSEPayload extracts the data payload from one raw SSE line. It reports ok=false for a line that is not `data:` framing (e.g. an `event:` line or a blank separator) or whose payload is empty, both of which callers skip; otherwise it returns the payload with the "data:" prefix stripped and surrounding whitespace trimmed. It does not interpret provider sentinels such as "[DONE]".
func Send ¶
Send appends a message to an ordered per-run channel, deduped by key: a redelivery with the same (runID, channel, key) is a no-op and the first payload wins (at-most-once intake over at-least-once transport). Safe to call from any process; the store's PK / ON CONFLICT is the cross-process dedup, exactly as for Signal.
func Signal ¶
Signal delivers a single-shot signal to a run, journaled at-most-once by name: a redelivery (a retried webhook, an at-least-once queue) is a no-op and the first payload wins. This turns at-least-once transport into exactly-once application to the run. Safe to call from any process; the store's primary-key / ON CONFLICT is the cross-process dedup.
Signal only records the payload. After delivering, re-invoke Run with the same runID to resume the awaiting run: directly, or via a Waker scheduled at the current time.
func Sleep ¶
Sleep pauses the current run until d has elapsed from the FIRST time this timer was reached, durably. Call it from inside a retry-safe tool (the agent loop supplies the run context). On the first encounter it journals the wake time (now + d) and returns *Sleeping, pausing the run; on a later resume it returns nil once the wake time has passed, and continues past this point. The wake time is fixed on the first call and memoized, so a resumed or crash-recovered run waits to the same absolute instant rather than restarting the clock. Use distinct names for distinct timers. Sleep requires a retry-safe tool (Safety.ReadOnly or Idempotent), like Interrupt.
func Step ¶
func Step[T any](ctx context.Context, d Durable, runID, name string, fn func(context.Context) (T, error)) (T, error)
Step runs fn as a named durable step and returns its typed result. On resume, a completed step returns its recorded result without re-running fn. This is the Option-B authoring primitive: write plain Go control flow, and name the operations that must survive a crash.
inv, err := agent.Step(ctx, dur, runID, "fetch-invoice", func(ctx context.Context) (Invoice, error) { ... })
T must be JSON-serializable: the result is marshaled into the journal, so a struct with unexported fields round-trips those fields to their zero values (encoding/json skips them) with no error reported. Return exported fields, a map, or a pointer whose fields are exported.
func TallyApprovals ¶ added in v0.4.0
func TallyApprovals(recs []Record, s ApprovalSubject, p ApprovalPolicy, verifierFor ApproverVerifierFor) (ApprovalTally, []DecisionCheck)
TallyApprovals is the m-of-n counting rule, shared by the gate and offline verification so the two cannot drift. Over recs, in journal order, it considers every decision record on s.ToolUseID (IsApprovalDecision). An approver's decision is their FIRST record that is valid: the approver is in p.Approvers, verifierFor resolves their key, and the signature verifies over ApprovalDecisionBytes(s, approver, approved). Records that are not valid never occupy an approver's place, so a forged or mistaken decision cannot block the approver's real one; later valid records from an approver who already decided are superseded.
It returns the tally and a classification of every record it considered. It does not validate p (see ApprovalPolicy.Validate); a nil verifierFor counts nothing.
func WaitUntil ¶
WaitUntil pauses the current run until the absolute time `until`, durably. It is the deadline form of Sleep: same semantics, but the wake time is the given instant rather than a relative delay.
func WithClock ¶
WithClock binds a clock to ctx for the durable timer to read "now". Deployments leave it unset (defaulting to time.Now); tests inject a controllable clock to advance time deterministically.
func WithIdentity ¶
WithIdentity binds an acting identity to ctx. The deployment calls this once at run start (for example, agent.Run(agent.WithIdentity(ctx, id), runID, input)); it then propagates to tool calls and, through the run scope, to sub-agents, so every governed action inherits the same attribution without threading it by hand.
Types ¶
type APIError ¶
APIError is returned by a provider adapter for a non-2xx HTTP response other than 429 (which uses RateLimited). It carries the StatusCode so a retry classifier can tell a transient failure (5xx, 408) from a terminal one (most 4xx: auth, validation), and a short Body snippet for diagnostics. It wraps Err (typically ErrModel) so errors.Is(err, ErrModel) holds.
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent binds a model, a tool set, a durable store, and a middleware chain.
The WithX/Use/SetX builder methods MUTATE the receiver in place and return it for chaining; they do not copy. This is deliberate: it lets configuration be applied after construction and after the value has been shared, e.g. a.Use(trace.Model(...)) or a.Use(agent.WithRetrieval(...)) wiring a cross-cutting concern onto an agent other code already holds. The consequence is that two variables assigned from the same New(...) are aliases: reconfiguring one reconfigures both. When you need an independently configured variant, construct a fresh Agent rather than expecting a builder to fork. (The internal clone/cloneWith, used by RunTyped, do copy; they are not exported.)
An Agent is safe for concurrent Run/Stream/Session calls once configured; the builder methods are not safe to call concurrently with a run or with each other. Configure first, then run.
func New ¶
New constructs an Agent. It panics if model or store is nil: both are load-bearing on every run (the model drives turns, the store journals them for at-most-once resume), so a nil is a construction-time programmer error, not a runtime condition to thread through every call.
func (*Agent) Run ¶
Run drives the agent to completion for runID, resuming from the journal if steps already exist. Completed steps are reused; retry-safe tools with no recorded result are re-run; a non-retry-safe tool with no result triggers ResumeHalt; a tool that requires approval with no recorded decision triggers PendingApproval. A run that already finished is final: Run returns its recorded answer without calling the model, whatever input is passed, so retrying a completed run never repeats its side effects.
func (*Agent) RunResult ¶
RunResult is the envelope-returning counterpart of Run. It drives the agent to completion and returns a *Result carrying the final message plus accumulated telemetry (usage totals, turn count, duration). Run remains unchanged; existing callers need not change.
func (*Agent) RunSaga ¶
RunSaga runs the agent as a transaction: on success it behaves like Run; if a step fails after earlier writes succeeded, it compensates the completed writes in reverse order (recursing into sub-agent trees) and returns *SagaAborted.
The abort is derived from the journal (a durable StepSagaFail record), so a crash at any point resumes correctly: on re-entry a recorded failure sends us straight to rollback, and each compensation is itself a durable memoized step (at-most-once).
Note: if a non-retriable step's outcome is genuinely unknown (crashed after its attempt marker but before any result), resume returns *ResumeHalt instead — you can't safely auto-roll-back a step that may have committed; a human decides.
func (*Agent) RunSagaResult ¶
RunSagaResult is the envelope-returning counterpart of RunSaga. It behaves identically to RunSaga (transactional run with reverse-order compensation on failure) but returns the richer *Result envelope on success.
func (*Agent) Session ¶
Session opens (or reopens) a multi-turn conversation with the given id, rebuilding the transcript from the store so a restarted process continues where it left off.
func (*Agent) SetMaxConcurrency ¶
SetMaxConcurrency bounds how many tool calls run in parallel within a single turn. n <= 0 means unbounded (the default). Returns the agent for chaining; use SetMaxConcurrency(1) to force fully sequential tool execution.
func (*Agent) Stream ¶
func (a *Agent) Stream(ctx context.Context, runID, input string) *AgentStream
Stream drives the agent like Run but returns a live AgentStream: token deltas, turn boundaries, and tool start/finish arrive as events while the durable loop runs. Resume, approval, and side-effect safety are identical to Run — Stream and Run share one loop; Run is Stream(...).Final().
Example ¶
ExampleAgent_Stream shows the streaming API: range Events for live progress, then call Final for the terminal answer. Run is the blocking equivalent of Final.
// A model that answers directly (a real adapter streams token deltas the same way).
m := &scriptModel{turns: [][]Emit{textTurn("hello world")}}
a := New(m, NewMemStore())
stream := a.Stream(context.Background(), "run-1", "hi")
for ev := range stream.Events() {
switch e := ev.(type) {
case TurnStarted:
fmt.Println("turn started")
case ModelEvent:
if d, ok := e.Event.(TextDelta); ok {
fmt.Printf("delta: %q\n", d.Text)
}
case Finished:
fmt.Println("finished")
}
}
answer, err := stream.Final()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("answer:", textOf(answer))
Output: turn started delta: "hello world" finished answer: hello world
func (*Agent) StreamSaga ¶
func (a *Agent) StreamSaga(ctx context.Context, runID, input string) *AgentStream
StreamSaga is the streaming counterpart of RunSaga (transactional run with reverse- order compensation on failure).
func (*Agent) Use ¶
func (a *Agent) Use(mw ...Middleware) *Agent
Use appends middleware wrapping the model call (first added = outermost). Returns the agent for chaining.
func (*Agent) UseTool ¶
func (a *Agent) UseTool(mw ...ToolMiddleware) *Agent
UseTool appends tool middleware wrapping every tool call (first added = outermost). Returns the agent for chaining. Composes with model middleware (Use) independently: Use wraps the model call, UseTool wraps tool calls.
func (*Agent) WithApproverVerifiers ¶ added in v0.4.0
func (a *Agent) WithApproverVerifiers(fn ApproverVerifierFor) *Agent
WithApproverVerifiers configures how the m-of-n approval gate resolves an approver id to the verifier for its signature. Required whenever any tool carries a non-nil Safety.Approval: a gated call with no resolver configured fails with ErrConfig rather than silently counting zero decisions. Returns the agent for chaining.
func (*Agent) WithMaxTurns ¶
WithMaxTurns caps the number of model turns a single run may take, so a model that keeps calling tools can't loop forever. n <= 0 means unbounded (the default). When the cap is reached the run returns ErrMaxTurns (category ErrBudget). Returns the agent for chaining. The cap is per run (per Session.Send turn), not per session.
func (*Agent) WithSampling ¶
func (a *Agent) WithSampling(opts ...SamplingOption) *Agent
WithSampling sets generation controls applied to every model call (last write wins per field). Returns the agent for chaining: New(...).WithSampling(agent.Temperature(0), agent.MaxTokens(500)).
func (*Agent) WithSystemPrompt ¶
WithSystemPrompt sets a system message that is prepended to the conversation on every model turn. The message is re-seeded on each Run (including resumes), so it is always present regardless of journal replay. Returns the agent for chaining: New(...).WithSystemPrompt("you are a concise assistant").
func (*Agent) WithSystemPromptFunc ¶
WithSystemPromptFunc sets a system message computed per run, so it can inject dynamic context (current date, tenant, retrieved state) each turn. It takes precedence over WithSystemPrompt. Returns the agent for chaining.
func (*Agent) WithToolChoice ¶
func (a *Agent) WithToolChoice(tc ToolChoice) *Agent
WithToolChoice sets the tool-choice control applied to every model call (see ToolChoice for the modes). Returns the agent for chaining.
CAVEAT: forcing Mode "required" or "tool" on every turn of the multi-turn loop keeps the model from ever producing a final answer (it must always call a tool), so the run cannot terminate normally. Reserve those modes for single-turn or typed/structured calls; "auto" (the default when unset) is the norm for the agent loop.
type AgentEvent ¶
type AgentEvent interface {
// contains filtered or unexported methods
}
AgentEvent is a lifecycle event emitted by Agent.Stream as the run loop advances: turn boundaries, live model token deltas, and tool start/finish. It is the SEMANTIC-layer companion to the model's byte-level Event — a UI ranges over these to render progress while the durable loop runs underneath.
The terminal answer and any error are NOT events: they come from AgentStream.Final (mirroring how the model Stream yields Events but returns the assembled Message separately). A run that pauses for approval or halts on an unsafe resume emits the relevant lifecycle event and then surfaces the *PendingApproval / *ResumeHalt via Final, exactly as Run returns it.
func ReplayEvents ¶
ReplayEvents returns the semantic lifecycle events implied by runID's DURABLE journal: the same AssistantTurn and ToolCompleted events Agent.Stream re-emits when it resumes from that journal, in persisted order. Because the sequence is a pure function of the recorded steps, it — and any commitment built over it (see audit.EventLogFromJournal) — is identical before and after a crash, which is what makes it a resume-stable audit artifact.
It reconstructs from the journal alone, without re-running the model or tools. Only journaled facts are reproduced: assembled assistant turns (StepModel) and completed tool calls with their results (StepToolResult). Live-loop-only signals — token-level ModelEvent deltas, TurnStarted, ToolStarted, and the terminal Finished — are not journaled and so are not part of the durable projection; the durable content is the turns and tool results.
type AgentStream ¶
type AgentStream struct {
// contains filtered or unexported fields
}
AgentStream is a live view of a running agent: range Events for progress, then call Final for the terminal answer (or error). It is the streaming counterpart of Run, the same way the model Stream is the counterpart of Generate — Run is literally Stream(...).Final().
A Stream is consumed once. Either range Events fully (Final then returns immediately) or call Final directly (it drains and discards events); calling Final after breaking out of Events early drains whatever remains. Cancel ctx to abandon a run without draining — pending emits then unblock on ctx.Done rather than leaking.
func (*AgentStream) Events ¶
func (as *AgentStream) Events() iter.Seq[AgentEvent]
Events returns a range-over-func iterator over lifecycle events until the run ends. Per-event errors do not occur here (a mid-stream model error aborts the run and is reported by Final); the iterator simply ends when the run stops producing events.
func (*AgentStream) Final ¶
func (as *AgentStream) Final() (Message, error)
Final drains any un-consumed events and returns the run's terminal message and error (including *PendingApproval / *ResumeHalt, matching Run). Safe to call after fully or partially ranging Events, or on its own.
type ApprovalPolicy ¶ added in v0.4.0
type ApprovalPolicy struct {
Need int // decisions required to proceed (k), 1 <= Need <= len(Approvers)
Approvers []string // eligible approver ids; the bounded set (n)
}
ApprovalPolicy declares a k-of-n human gate. Need is k; the eligible approvers are the bounded set (n = len(Approvers)). A tool with a non-nil Approval requires approval whether or not RequiresApproval is also set.
func (ApprovalPolicy) Validate ¶ added in v0.4.0
func (p ApprovalPolicy) Validate() error
Validate reports whether the policy is well formed: at least one approver, no empty or duplicate approver ids, and 1 <= Need <= len(Approvers). A policy that fails is a configuration error; the gate refuses it with ErrConfig rather than guessing.
type ApprovalRequired ¶
type ApprovalRequired struct {
ToolUseID string
Name string
Args json.RawMessage
// Quorum is the running tally when the tool has an m-of-n Safety.Approval policy, so a
// streaming UI can show progress ("1 of 2 approved") without waiting for Final. Nil for
// a 1-of-1 gate.
Quorum *ApprovalTally
}
ApprovalRequired fires immediately before the run pauses for a human decision on a tool that requires approval. The run then returns *PendingApproval from Final; record a decision (Approve, or ApproveAs for an m-of-n gate) and re-invoke to continue.
type ApprovalSubject ¶ added in v0.4.0
type ApprovalSubject struct {
RunID string
ToolUseID string
ToolName string
Args json.RawMessage
}
ApprovalSubject is exactly what an approver decides on: one tool call, with its name and arguments, in one run. The decision bytes bind all of it, so a signature approves this call and no other: not another run, not another call, and not the same call id with different arguments.
type ApprovalTally ¶ added in v0.4.0
type ApprovalTally struct {
Need int `json:"need"` // the policy's k
Approvers []string `json:"approvers"` // the eligible set the gate enforced, in policy order
Approved int `json:"approved"` // approvers whose counted decision is an approval
Denied int `json:"denied"` // approvers whose counted decision is a denial
ApprovedBy []string `json:"approved_by,omitempty"` // those approvers, in journal order
DeniedBy []string `json:"denied_by,omitempty"` // those approvers, in journal order
Pending []string `json:"pending,omitempty"` // eligible approvers with no valid decision yet, in policy order
// Records names every decision record on this call the gate read, valid or not, in
// journal order. Evidence must disclose all of them, so an omitted decision is detectable.
Records []string `json:"records,omitempty"`
}
ApprovalTally is the m-of-n gate's count for one call. At a pause it is the running tally (PendingApproval.Quorum, ApprovalRequired.Quorum). At a terminal outcome the gate journals it under ApprovalTallyStep, before the tool runs, as the record of what it enforced and what it read.
func (ApprovalTally) Passed ¶ added in v0.4.0
func (t ApprovalTally) Passed() bool
Passed reports whether Need approvals are in.
func (ApprovalTally) Unreachable ¶ added in v0.4.0
func (t ApprovalTally) Unreachable() bool
Unreachable reports whether Need can no longer be reached: fewer approvers remain who have not denied than approvals are required. Only valid denials count toward this, so an invalid record cannot force a denial.
type ApproveOption ¶ added in v0.4.0
type ApproveOption func(*approveOptions)
ApproveOption configures ApproveAs.
func WithDecisionCheck ¶ added in v0.4.0
func WithDecisionCheck(verifierFor ApproverVerifierFor) ApproveOption
WithDecisionCheck makes ApproveAs check the decision before recording it, so an approver learns at submission time that it will not count: it reads the run, finds the call, and verifies the signature against that exact call with verifierFor. It returns ErrInvalidApproval (no such call, unknown approver, or a signature that does not verify) or ErrAlreadyDecided (the approver's earlier valid decision already counts), and records nothing. The check is for feedback; the gate re-verifies every record itself, so correctness never depends on it. Eligibility is the policy's, which ApproveAs does not see.
type ApproverVerifier ¶ added in v0.4.0
ApproverVerifier checks an approver's signature over the decision bytes. Any audit.Verifier satisfies it structurally, so a deployment reuses the existing ed25519/ML-DSA/hybrid verifiers with no new cryptography and no agent->audit import.
type ApproverVerifierFor ¶ added in v0.4.0
type ApproverVerifierFor func(approverID string) (ApproverVerifier, bool)
ApproverVerifierFor resolves the verifier for an approver id (the caller's PKI, mirroring audit's issuer lookup). It reports ok=false for an unknown approver; the counting rule treats that approver's decisions as unverifiable.
type AssistantTurn ¶
AssistantTurn is the fully-assembled assistant message for a turn. Replayed is true when it was reconstructed from the journal on resume rather than produced by a live model call (in which case no ModelEvent deltas preceded it).
type Awaiting ¶
type Awaiting struct {
RunID string
Name string
Prompt any // optional caller payload describing what the run is waiting for
}
Awaiting is returned by Run when a tool called Await and no signal has been delivered for that name yet. The run has paused durably at the await point. Deliver a signal with Signal (same name), then re-invoke Run with the same runID to continue. Awaiting is the externally-pushed dual of Interrupted: Interrupt asks a human and resumes with their answer; Await waits for an event an outside system delivers.
type Compensator ¶
type Compensator interface {
// Compensate undoes a completed call. args are the tool's original arguments; result
// is what Call returned. Must be idempotent — on a crash mid-rollback it may re-run.
Compensate(ctx context.Context, args, result json.RawMessage) error
}
Compensator is an optional interface a Tool implements to declare how to UNDO its side effect. In a saga run (RunSaga), if a step fails after earlier writes succeeded, the completed compensatable writes are rolled back in reverse order — automatically, and recursively through sub-agent trees.
This is the transactional / saga agent: "charged the card and booked the flight, then failed on the hotel → cleanly refund + cancel." It's the gsm compensation idea applied to the agent runtime — the journal records exactly which writes completed, so well-founded reverse compensation falls out of the durable substrate.
A saga step must be ATOMIC: it must not leave a partial external side effect before returning an error, because the failing step itself is not compensated (there's no recorded result to drive Compensate). Make forward steps all-or-nothing, or idempotent.
type DecisionCheck ¶ added in v0.4.0
type DecisionCheck struct {
Step string // the record's journal name
Approver string
Approved bool
Counted bool // this is the approver's decision: their first valid one
Reason string // why it did not count; empty when Counted
}
DecisionCheck is how the counting rule classified one decision record.
type Doc ¶
type Doc struct {
ID string `json:"id,omitempty"`
Text string `json:"text"`
Score float64 `json:"score,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
Doc is a document returned by a Retriever: its text plus optional id, similarity score, and metadata. This is the neutral shape the retrieval helpers speak; your Retriever maps your store's results onto it.
type Durable ¶
type Durable interface {
// Do returns the recorded Record for (runID, name) without running fn if present;
// otherwise runs fn, records the returned Record (with Name set), and returns it.
// If fn errors, nothing is recorded — the step re-runs on the next attempt.
Do(ctx context.Context, runID, name string, fn func(context.Context) (Record, error)) (Record, error)
// History returns all recorded steps for a run, in order.
History(ctx context.Context, runID string) ([]Record, error)
}
Durable is the crash-safe substrate: named-step memoization. Do runs a step at most-once per (runID, name): a recorded step returns without re-running fn. This mirrors DBOS RunAsStep and ADK v2 RunNode; our SQLite default implements it directly, and store/postgres is the high-availability backend. The side-effect-safety layer (Safety / ResumeHalt) sits ABOVE this and is substrate-agnostic (see Agent.Run).
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is a normalized streamed model event. The stream/ package maps every provider wire format (OpenAI SSE / Anthropic typed / Gemini NDJSON / Bedrock binary) onto these.
type Finished ¶
type Finished struct{ Final Message }
Finished carries the terminal assistant answer — the same Message that Run returns and that Final reports. The event stream closes after this.
type HaltTooYoung ¶ added in v0.3.0
type HaltTooYoung struct {
RunID string
ToolUseID string
Age time.Duration // elapsed since the effect was attempted
Min time.Duration // the required minimum
}
HaltTooYoung is returned by ResolveHalt when WithMinHaltAge is set and the halt has not aged past the grace period yet. Wait and retry the resolution later.
func (*HaltTooYoung) Error ¶ added in v0.3.0
func (e *HaltTooYoung) Error() string
type Identity ¶
type Identity struct {
// Actor is the acting agent instance: a deployment plus model version, e.g. "exec-agent@1.4.2".
Actor string `json:"actor,omitempty"`
// OnBehalfOf is the principal the agent acts for: a desk, an account, a user.
OnBehalfOf string `json:"on_behalf_of,omitempty"`
// AuthorityRef identifies the grant that authorizes this action: the digest or id of a signed
// capability/authority record (see audit anchoring), so the leaf links to what permitted it.
AuthorityRef string `json:"authority_ref,omitempty"`
}
Identity is the accountability binding for a run: which agent instance acted, on whose behalf, and under what authority. It is assigned by the DEPLOYMENT, not by the model: the operator sets it at run start from its own auth layer (an IdP, a signed capability grant, a service identity), and it rides the context so every governed action and every sub-agent call inherits it. The SDK anchors and proves the identity CLAIM (these fields, committed in the audit log); it does not authenticate the principal, which is the operator's IdP/PKI, and the attribution is only as strong as custody of whatever key signs the run's tree heads and grants.
The model has no inherent identity. "The agent's identity" is the identity the deployment binds to this run, so a compromised or misconfigured deployment attributes to whatever it claims. That boundary is deliberate and matches the model/governance split: prove the record and the granted authority, do not overclaim the intent behind them.
func IdentityFrom ¶
IdentityFrom returns the acting identity bound to ctx, and whether one was set.
type Image ¶
type Image struct {
Mime string `json:"mime,omitempty"` // MIME type for Data (required when Data is set), e.g. "image/jpeg"; ignored for URL
Data []byte `json:"data,omitempty"` // raw image bytes; adapters base64-encode. Mutually exclusive with URL.
URL string `json:"url,omitempty"` // hosted image URL. Mutually exclusive with Data.
}
Image is a provider-neutral image input. Exactly one of Data or URL is set (Data XOR URL). For raw bytes, set Data and Mime (e.g. "image/png"); the adapter base64-encodes it (Anthropic base64 source / OpenAI data URI). For a hosted image, set URL and leave Data nil; Mime is not required for URLs. This is an input-only part: models emit text, reasoning, and tool calls, never images, so nothing produces an Image on the response path.
type Interrupted ¶
type Interrupted struct {
RunID string
Key string
Prompt any // caller-defined payload for the human: a question, options, current state
}
Interrupted is returned by Run when a tool called Interrupt and no resume value has been recorded for that key yet. The run has paused durably at the interrupt point. Inspect Prompt to decide what to ask the human, record an answer with Resume (same Key), then re-invoke Run with the same runID to continue.
func (*Interrupted) Error ¶
func (e *Interrupted) Error() string
type JSONToolResultCodec ¶ added in v0.2.0
type JSONToolResultCodec struct{}
JSONToolResultCodec is the default codec: it passes the canonical JSON through unchanged. It is equivalent to what an adapter does when no ToolResultCodec is configured.
func (JSONToolResultCodec) EncodeToolResult ¶ added in v0.2.0
func (JSONToolResultCodec) EncodeToolResult(raw json.RawMessage) (string, error)
EncodeToolResult returns the JSON bytes as a string, unchanged.
type Leaser ¶
type Leaser interface {
// AcquireLease claims runID for holder until now+ttl. It returns true if granted (the run is
// unleased or the live lease is already holder's, which renews it), false if another holder
// currently holds a live lease. An expired lease is available to any holder.
AcquireLease(ctx context.Context, runID, holder string, ttl time.Duration) (bool, error)
// RenewLease extends holder's lease on runID, returning false if holder no longer holds it (it
// expired or was taken). A driver whose work outlasts ttl renews to keep the lease.
RenewLease(ctx context.Context, runID, holder string, ttl time.Duration) (bool, error)
// ReleaseLease relinquishes runID if held by holder (a no-op otherwise) so another process can
// take it immediately rather than waiting for expiry.
ReleaseLease(ctx context.Context, runID, holder string) error
}
Leaser is the optional capability that coordinates driving a run across processes. Without it, several processes recovering against a shared store all re-drive the same in-flight runs. That is safe but wasteful: side effects stay at-most-once because each non-idempotent call is guarded by an exclusive attempt claim (ClaimAttempt), but the drivers duplicate model calls and a loser of a claim halts with ResumeHalt. A store that implements Leaser lets a driver take a time-bounded lease on a run so normally only the holder drives it; a dead holder's lease expires and another process takes over, which is the high-availability property. Recover uses it automatically when the store provides it. A lease is an efficiency and liveness mechanism, not the safety one: no lease can guarantee mutual exclusion against a holder that stalls past its TTL, which is why the claim, not the lease, is what keeps side effects at-most-once.
The base Durable contract does not require leasing, and MemStore's implementation is in-process (for tests and as the reference); the cross-process payoff is a shared backend (store/postgres) implementing this with an atomic upsert over a leases table.
type Lister ¶
type Lister interface {
// Runs returns the IDs of every run the store holds, in no guaranteed order.
Runs(ctx context.Context) ([]string, error)
}
Lister is the optional capability a crash-recovery supervisor needs: it enumerates the runs a store knows about, so Recover can find in-flight runs to re-drive after a restart. The base Durable interface intentionally does NOT require it: memoization (Do) and replay (History) are the crash-safety core, and enumeration is a separate, backend-specific concern (a SQL store lists with a query; a sharded store may not enumerate cheaply at all). A store opts in by implementing Runs; Recover type-asserts for it.
type MemStore ¶
type MemStore struct {
// contains filtered or unexported fields
}
MemStore is an in-memory Durable for tests and local dev. SQLite is the shipping default; store/postgres is the high-availability backend.
It keeps each record as its JSON encoding and decodes on every read, the same way the SQL stores do. So a record a caller gets back is always an independent copy (modifying it cannot change the journal), and it has the same byte-normalized form a production store returns, which keeps tests on MemStore faithful to SQLite and Postgres.
func NewMemStore ¶
func NewMemStore() *MemStore
func (*MemStore) AcquireLease ¶
func (m *MemStore) AcquireLease(_ context.Context, runID, holder string, ttl time.Duration) (bool, error)
AcquireLease implements Leaser.
func (*MemStore) ReleaseLease ¶
ReleaseLease implements Leaser.
type MemWaker ¶
type MemWaker struct {
// contains filtered or unexported fields
}
MemWaker is an in-process Waker: it holds pending timers and, when Fire is called with the current time (or via a Start ticker), resumes every run whose timer is due by calling the resume function it was built with. resume is how the deployment re-invokes a run, typically agent.Run(ctx, runID, savedInput); a resumed run whose wait is over proceeds, and one that sleeps again re-registers a new wake here. MemWaker is a reference and a local-dev default, not a durable scheduler: a process exit loses its in-memory timer set, so the wake times must also live in the durable journal (they do, Sleep journals them), and a restarted deployment rebuilds pending wakes by scanning runs, or an external scheduler owns the trigger.
func NewMemWaker ¶
NewMemWaker builds an in-process waker that resumes due runs via resume.
func (*MemWaker) Fire ¶
Fire resumes every run that has a timer due at or before now, each run once even if several of its timers are due, drops the fired timers, and returns how many runs it resumed. Resume callbacks run without the lock held, so a resumed run may register a new wake. Errors from resume are joined.
func (*MemWaker) Schedule ¶
Schedule registers a wake, idempotent per (runID, name): re-scheduling the same timer overwrites its fire time rather than adding a duplicate.
type Message ¶
Message is one turn. Its content is a sequence of typed Parts (NOT a flat string), so reasoning/thinking parts (which Anthropic requires echoed back across turns), tool calls, tool results, and image inputs all survive round-trips intact. This is the provider-neutral type; adapters translate to/from provider wire formats.
func SystemText ¶
func UserParts ¶
UserParts builds a user message from mixed parts, e.g. a prompt and one or more images: UserParts(Text{"what is this?"}, ImageData("image/png", raw)). It is the multimodal counterpart to UserText.
func (Message) MarshalJSON ¶
func (Message) Text ¶
Text returns the concatenation of the message's Text parts (ignoring reasoning, tool calls, and tool results). Empty if the message carries no text.
func (*Message) UnmarshalJSON ¶
type Middleware ¶
type Middleware func(ModelHandler) ModelHandler
Middleware wraps a ModelHandler — the net/http-style func(Handler) Handler chain, at the SEMANTIC layer (it sees messages, tool calls, token usage — not bytes). Batteries live in the middleware/ package (Retry, TokenBudget, ...).
func WithRetrieval ¶
func WithRetrieval(r Retriever, k int) Middleware
WithRetrieval is model middleware that auto-injects retrieved context (classic RAG): when the model is responding to a fresh user turn, it retrieves the top-k documents for that user message and prepends them as a system message. It does NOT retrieve on tool-result turns (mid-loop). A retrieval error aborts the model call — have your Retriever return (nil, nil) instead of an error if you prefer to degrade to no context.
type Model ¶
Model is the provider primitive. Streaming is FIRST-CLASS: Stream is the only method an adapter must implement. Generate is a convenience drain built on top — the opposite of frameworks that make blocking generation primary and bolt streaming on later.
func Replay ¶
Replay returns a Model that re-emits the model outputs recorded in `source` for runID, in order, instead of calling a live LLM. Run an agent with it against a FRESH store to deterministically re-execute a past run:
- time-travel debugging: step through exactly what happened, offline and free;
- regression tests: capture a production run, replay it in CI (pairs with testing/synctest), assert behavior didn't drift;
- evals over real traffic: the journal IS a golden dataset.
Because the journal captures every model output across the whole (possibly nested) tree, the replay is exact. This is only possible because the durable substrate records a complete, replayable history in the first place.
type ModelEvent ¶
type ModelEvent struct{ Event Event }
ModelEvent forwards one live model stream Event (TextDelta, ReasoningDelta, ToolCallDelta, Finish) from the current turn — the token-by-token feed. Emitted only for a FRESH model call; on durable replay the turn is reused from the journal and produces no deltas (an AssistantTurn with Replayed=true is emitted instead).
type ModelHandler ¶
ModelHandler generates one assistant turn. Middleware wraps it.
type Part ¶
type Part interface {
// contains filtered or unexported methods
}
Part is a single piece of message content. Concrete parts: Text, Reasoning, ToolUse, ToolResult, Image.
type PendingApproval ¶
type PendingApproval struct {
RunID string
ToolUseID string
ToolName string
Args json.RawMessage
// Quorum is non-nil for an m-of-n gate: the running tally at the pause. Record
// decisions with ApproveAs, then re-run.
Quorum *ApprovalTally
}
PendingApproval is returned by Agent.Run when a tool requiring human approval has no recorded decision yet. The run has paused durably; call Approve then re-run to resume.
func (*PendingApproval) Error ¶
func (e *PendingApproval) Error() string
func (*PendingApproval) Subject ¶ added in v0.4.0
func (e *PendingApproval) Subject() ApprovalSubject
Subject returns the approval subject of the paused call, which is what approvers sign:
sig := signer.Sign(agent.ApprovalDecisionBytes(pend.Subject(), "finance", true))
For a gate inside a sub-agent, RunID is the sub-run's id.
type RateLimited ¶
RateLimited is returned by a provider adapter when it receives HTTP 429. It carries an optional RetryAfter hint from the Retry-After response header (0 means no hint was provided). It wraps the caller-supplied Err (typically wrapping ErrModel) so errors.Is(err, ErrModel) holds.
func (*RateLimited) Error ¶
func (e *RateLimited) Error() string
func (*RateLimited) Unwrap ¶
func (e *RateLimited) Unwrap() error
type Reasoning ¶
Reasoning is model thinking. Signature is the provider's opaque token that must be echoed back on later turns (Anthropic extended thinking); dropping it corrupts the conversation — which is exactly why Content can't be a flat string.
type ReasoningDelta ¶
type Received ¶
Received is one message pulled from a channel.
func Receive ¶
Receive returns the oldest not-yet-acked message on channel, in delivery order. If the channel has no unacked message it returns *Awaiting and the run pauses durably. The run must Ack(channel, msg.Key) after it has durably handled the message; until then Receive keeps returning the SAME message, which is what makes processing replay-safe and exactly-once. Call from a retry-safe tool (Safety.ReadOnly or Idempotent), like Await.
type Record ¶
type Record struct {
Name string `json:"name"`
Kind StepKind `json:"kind"`
Message *Message `json:"message,omitempty"` // StepModel
ToolUseID string `json:"tool_use_id,omitempty"` // StepToolResult
Result json.RawMessage `json:"result,omitempty"` // StepToolResult / StepValue
IsError bool `json:"is_error,omitempty"` // StepToolResult
Approved bool `json:"approved,omitempty"` // StepApproval
Approver string `json:"approver,omitempty"` // StepApproval written by ApproveAs
Signature []byte `json:"signature,omitempty"` // StepApproval written by ApproveAs
// AttemptedAt is the Unix-millis wall-clock time an attempt marker (StepAttempt) was
// written, i.e. just before a non-retriable side effect fired. It is set once and read
// back verbatim on replay, so it stays deterministic. Zero (and omitted) on every
// other kind; a reconciler uses it to honor a grace period before resolving a halt.
AttemptedAt int64 `json:"attempted_at,omitempty"`
// Reconciled marks a StepToolResult that ResolveHalt injected from verified evidence
// rather than one the tool produced by running. It lets a later reader (and bide-audit)
// tell a reconciled outcome from a clean one at a glance.
Reconciled bool `json:"reconciled,omitempty"`
// Evidence is what a reconciler read to decide the outcome (a queried provider record,
// a message id, a log line). It is carried on the reconciled result and signed with it,
// so the verdict and its basis live in the journal beside the outcome.
Evidence json.RawMessage `json:"evidence,omitempty"`
// Claim is the random id of the driver that wrote an attempt marker (see ClaimAttempt).
// A driver runs the side effect only if the marker it gets back carries its own claim, so
// two drivers of the same run can never both run it, whatever their leases say.
Claim string `json:"claim,omitempty"`
}
Record is one durably-recorded, named event. The ordered sequence of Records for a run is its full, replayable history; resume rebuilds state by replaying them. Name is the stable identity used for memoization (see Durable.Do).
func ClaimAttempt ¶ added in v0.5.0
func ClaimAttempt(ctx context.Context, d Durable, runID, name string, rec Record) (won bool, got Record, err error)
ClaimAttempt writes the attempt marker named name as an exclusive claim and reports whether the caller won it. It stamps rec with a fresh random claim id and records it with Do; because Do records a name at most once, exactly one driver's marker is stored, and a driver won only if the marker Do returns carries its own claim. A driver that loses must not run the side effect the marker guards: another driver owns it and may be running it right now.
This makes at-most-once independent of leasing. A lease cannot guarantee mutual exclusion (a holder stalled past its TTL wakes still believing it holds the lease), but two drivers that overlap still cannot both win the claim, since the store's atomic insert decides the winner. With a single driver the claim is always won, so a run with no contention behaves exactly as before.
type RecoverOption ¶
type RecoverOption func(*recoverConfig)
RecoverOption configures Recover. Lease options apply only when the store implements Leaser.
func WithLeaseHolder ¶
func WithLeaseHolder(id string) RecoverOption
WithLeaseHolder sets the identity this recoverer claims leases under. Defaults to a host/pid/random string. Give each process a stable, distinct holder if you want lease ownership to survive a restart of the same logical worker.
func WithLeaseTTL ¶
func WithLeaseTTL(d time.Duration) RecoverOption
WithLeaseTTL sets how long an acquired lease is valid. Recover renews it while a run is driving, so a crash lets the lease expire after roughly this long and another process takes over. Defaults to 30s. Set it comfortably above the store's clock skew.
type Request ¶
type Request struct {
Messages []Message
Tools []Tool
Sampling Sampling // generation controls; zero value = provider/model defaults
ResponseFormat *ResponseFormat // nil = free-form; set = constrain output to a JSON schema
ToolChoice *ToolChoice // nil = provider default (auto); see ToolChoice
}
Request is a single model call.
type ResolveOption ¶ added in v0.3.0
type ResolveOption func(*resolveConfig)
ResolveOption configures ResolveHalt.
func WithEvidence ¶ added in v0.3.0
func WithEvidence(v any) ResolveOption
WithEvidence records the resolution as reconciled from verified evidence rather than an operator's bare assertion: it marks the injected result Reconciled and stores v (JSON-marshalled) as its Evidence, signed alongside the outcome. Use it so a later reader can tell a reconciled step from a clean one and re-check the basis of the verdict.
func WithMinHaltAge ¶ added in v0.3.0
func WithMinHaltAge(d time.Duration) ResolveOption
WithMinHaltAge refuses to resolve a halt younger than d, measured from the attempt marker's AttemptedAt to now. A reconciler passes this so it cannot resolve before the provider's record has had time to settle (a sent-message id can appear seconds after the send): resolving too early reads "absent" and re-fires the very side effect the halt exists to prevent. d <= 0 skips the check. When d > 0 but no attempt timestamp is found, ResolveHalt errors rather than resolve blind. Returns *HaltTooYoung when the halt has not aged enough, so the caller waits and retries later.
func WithNow ¶ added in v0.3.0
func WithNow(now func() time.Time) ResolveOption
WithNow overrides the clock WithMinHaltAge measures against (default time.Now). For tests and callers that carry their own clock.
type ResponseFormat ¶
type ResponseFormat struct {
Name string
Schema json.RawMessage
}
ResponseFormat asks the provider to constrain the model's output to a named JSON schema (OpenAI structured outputs / "json_schema" response format). Adapters that don't support it ignore it. Name is the schema name; Schema is provider-neutral JSON Schema (as from the schema package). See RunTypedNative.
type Result ¶
type Result struct {
// Message is the final assistant answer, identical to what Run / RunSaga return.
Message Message
// Usage is the sum of all model-call usages across the run (input, output, and
// cache tokens accumulated across every turn).
Usage Usage
// Turns is the number of live model turns executed during this run (replayed turns
// from the durable journal are not counted, since their usage was already accounted
// for in the original run).
Turns int
// Duration is the wall-clock elapsed time for the run (from entry to return).
Duration time.Duration
// RunID echoes the run identifier passed to RunResult / RunSagaResult.
RunID string
}
Result is a rich envelope returned by RunResult and RunSagaResult. It carries the terminal assistant message plus telemetry accumulated over the whole run: total token usage (sum of every model-call Usage), the number of model turns, wall-clock duration, and the run ID.
Run and RunSaga are unchanged and remain the idiomatic path for callers that only need the final message; RunResult / RunSagaResult are the additive counterparts for callers that need observability data.
type ResumeHalt ¶
type ResumeHalt struct {
RunID string
ToolUseID string
ToolName string
// AttemptedAt is when the effect was attempted (the attempt marker's timestamp), zero
// if unknown. A reconciler uses it to honor a grace period before resolving (see
// ResolveHalt with WithMinHaltAge) so it does not query the provider before its record
// has settled.
AttemptedAt time.Time
}
ResumeHalt is returned when resume can't safely proceed: a non-retriable tool was invoked but no result was recorded, so its outcome is unknown. The run stops for confirmation rather than risk a double side effect (e.g. a double charge).
It is also returned when another driver of the same run claimed the call first (see ClaimAttempt), for example a second node that took over after this node's lease lapsed. That driver owns the side effect; once it records the result, re-running proceeds normally.
func (*ResumeHalt) Error ¶
func (e *ResumeHalt) Error() string
type Retriever ¶
Retriever is the bring-your-own-RAG port: given a query, return the top-k relevant documents from YOUR store (pgvector, Pinecone, a file index — anything). Bide ships no vector store and no embedder; you implement Retrieve against infrastructure you already run, and wire it in with RetrievalTool (agentic — the model searches on demand) or WithRetrieval (classic — top-k auto-injected as context on each user turn). Sessions already give conversational memory; this is the seam for semantic / long-term memory.
type Safety ¶
type Safety struct {
// ReadOnly: no external side effects — always safe to re-run.
ReadOnly bool
// Idempotent: mutates state but is safe to retry, because a repeat with the same
// key is a no-op downstream. Set IdempotencyKey to make that concrete.
Idempotent bool
// IdempotencyKey derives a stable key from the args so a retried call can be
// de-duplicated. Empty result => not derivable.
IdempotencyKey func(args json.RawMessage) string
// RequiresApproval pauses the run for a durable human decision (HITL) before the
// tool executes — surfaced as *PendingApproval; resume after agent.Approve.
RequiresApproval bool
// Approval, when non-nil, upgrades the approval gate from one
// decision to an m-of-n human gate over a bounded, named set of
// approvers. nil = the existing 1-of-1 RequiresApproval behavior.
Approval *ApprovalPolicy
}
Safety declares how a tool may be retried when a run resumes after a crash and the call's outcome is unknown (it was invoked, but no result was journaled). Replaces a coarse Read/Write flag. Maps directly onto MCP tool annotations (readOnlyHint / idempotentHint / destructiveHint) for MCP-sourced tools.
type SagaAborted ¶
type SagaAborted struct {
RunID string
Cause error
Compensated []string
Uncompensated []string
CompensateErr error
}
SagaAborted is returned by RunSaga when a step failed and the transaction was rolled back. Compensated lists tools whose side effects were undone (reverse of execution, including sub-agent trees). Uncompensated lists completed WRITES that had no compensator — dangling side effects needing manual cleanup. CompensateErr is non-nil if a compensator itself failed: rollback stopped, so writes before it remain uncompensated.
func (*SagaAborted) Error ¶
func (e *SagaAborted) Error() string
func (*SagaAborted) Unwrap ¶
func (e *SagaAborted) Unwrap() error
type Sampling ¶
type Sampling struct {
Temperature *float64 // 0..2 (OpenAI) / 0..1 (Anthropic); determinism at 0
TopP *float64 // nucleus sampling
MaxTokens *int // cap on generated tokens; overrides the adapter's construction default
Stop []string // stop sequences
Seed *int64 // best-effort determinism (OpenAI; ignored where unsupported)
}
Sampling holds provider-neutral generation controls. Pointer fields distinguish "unset" (nil → use the provider/model default) from an explicit value — so a deliberate Temperature of 0 is not confused with "not specified". Each adapter maps the set fields onto its wire format and ignores those it doesn't support (e.g. Anthropic has no Seed). Set it once with Agent.WithSampling.
type SamplingOption ¶
type SamplingOption func(*Sampling)
SamplingOption sets one field of the Sampling config; see Temperature, TopP, MaxTokens, Stop, Seed.
func MaxTokens ¶
func MaxTokens(v int) SamplingOption
MaxTokens caps generated tokens, overriding the model adapter's construction default.
func Seed ¶
func Seed(v int64) SamplingOption
Seed sets a best-effort determinism seed (honored by providers that support it).
func Temperature ¶
func Temperature(v float64) SamplingOption
Temperature sets the sampling temperature (0 = most deterministic).
type ScriptedModel ¶
type ScriptedModel struct {
// contains filtered or unexported fields
}
ScriptedModel is a deterministic, LLM-free Model that replays a fixed script of turns, one turn per model call. It is the batteries-included version of the model fakes people otherwise reverse-engineer from ToolCallDelta + Finish for tests, examples, and local dev, so you can drive the whole agent loop offline.
It is replay-safe: it selects the turn to emit by counting the assistant messages already in the request, not by an internal cursor. On a durable resume the agent loop replays journaled assistant turns without calling the model, so a cursor would drift; counting the transcript instead lines the script back up with the live turn every time, including after a crash and re-run. Once the script is exhausted it repeats its last turn, so an over-long conversation does not panic.
Build turns with ToolTurn, TextTurn, and ErrorTurn:
m := agent.NewScriptedModel(
agent.ToolTurn("call-1", "lookup", `{"q":"weather"}`),
agent.TextTurn("it is sunny"),
)
a := agent.New(m, agent.NewMemStore(), lookupTool)
out, err := a.Run(ctx, "run-1", "what's the weather?")
A ScriptedModel is safe for the sequential agent loop; it is not intended for concurrent Stream calls from multiple runs at once.
func NewScriptedModel ¶
func NewScriptedModel(turns ...ScriptedTurn) *ScriptedModel
NewScriptedModel builds a ScriptedModel that replays turns in order, one per model call.
type ScriptedTurn ¶
type ScriptedTurn struct {
// contains filtered or unexported fields
}
ScriptedTurn is one scripted model output. Construct one with ToolTurn, TextTurn, or ErrorTurn rather than building it directly.
func ErrorTurn ¶
func ErrorTurn(err error) ScriptedTurn
ErrorTurn scripts a turn that fails the model call with err, so a test can exercise the loop's error path (a mid-run "crash" before the next step is journaled, say).
func TextTurn ¶
func TextTurn(text string) ScriptedTurn
TextTurn scripts a turn in which the model emits a final text answer, ending the run.
func ToolTurn ¶
func ToolTurn(id, name, args string) ScriptedTurn
ToolTurn scripts a turn in which the model calls one tool: id is the tool-use ID the journal keys the call under, name is the tool, and args is its JSON arguments.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is a durable multi-turn conversation. Each Send is one full agent run (tools, resume, side-effect safety and all), seeded with the transcript so far — so the agent remembers earlier turns. The transcript is journaled turn-by-turn under the session id, so a Session reloaded (after a restart) from the same store resumes the conversation.
Layering: turn N runs under runID "<id>/tN" (its own durable journal handles crash resume WITHIN the turn); the session-level journal under "<id>" records each completed turn's (input, answer) so the transcript can be rebuilt. Intermediate tool calls stay in the turn's journal and are NOT carried into later turns — the conversational memory is the question/answer transcript, not every tool call.
func (*Session) History ¶
History returns a copy of the conversation transcript so far (alternating user and final-assistant messages).
func (*Session) Send ¶
Send runs one conversation turn: the agent answers `input` with the full prior transcript in context, and the turn is journaled. Returns the assistant's answer.
If the turn pauses (a tool needs approval or Interrupt), Send returns that error (*PendingApproval / *Interrupted) and does NOT advance the transcript; resolve it (Approve / Resume) and call Send again with the SAME input to resume that turn.
type Sleeping ¶
Sleeping is returned by Run when a tool called Sleep or WaitUntil and the wake time has not yet passed. The run has paused durably at the timer: its wake time is journaled, so the pause survives a restart. Re-invoke Run with the same runID at or after FireAt to resume (a Waker does this automatically; otherwise the deployment re-invokes on its own schedule).
type StepKind ¶
type StepKind string
StepKind records what a journal entry represents.
const ( StepModel StepKind = "model" // an assistant message from the model StepToolResult StepKind = "tool_result" // a completed tool call + its result StepValue StepKind = "value" // a user-authored durable step (see Step[T]) StepSignal StepKind = "signal" // an external event delivered into a run (see Signal/Await) StepApproval StepKind = "approval" // a durable human approve/deny decision (HITL) StepAttempt StepKind = "attempt" // "about to run a non-retriable side effect" marker StepSagaFail StepKind = "saga_fail" // a saga step failed → durable abort trigger )
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream is a live model response. Range over Events() for a streaming UI, OR call Message() to drain it into the fully-assembled assistant Message — not both (a Stream is consumed once).
type Task ¶
Task is one unit of durable parallel work: a named step returning a T. Name is the durable memoization key within the run, so it must be unique across the tasks in one Parallel call. Because each task is a journaled step, a resumed run returns a completed task's recorded result without re-running it, and that result is an independently provable record (see audit.ProveStep).
type Tool ¶
type Tool interface {
Name() string
Description() string
// ArgsSchema is the provider-NEUTRAL argument schema. The schema/ package emits
// per-provider dialects (OpenAI-strict / Gemini / Anthropic) from it — so this is
// never a single frozen blob handed straight to a provider.
ArgsSchema() json.RawMessage
// Safety declares how the tool may be retried on resume.
Safety() Safety
Call(ctx context.Context, args json.RawMessage) (json.RawMessage, error)
}
Tool is an action the agent can take. The interface is untyped (json.RawMessage) so heterogeneous tools share one type — including runtime MCP tools whose schema is only known at runtime and can't be a Go struct. Native tools get compile-time typing via Func (below); that's the primary ergonomic.
func CompensatedFunc ¶
func CompensatedFunc[In, Out any]( name, description string, safety Safety, do func(context.Context, In) (Out, error), undo func(context.Context, In, Out) error, ) Tool
CompensatedFunc is a typed tool that declares both its forward action and its compensator. undo receives the same typed input and the output do produced.
func Func ¶
func Func[In, Out any](name, description string, safety Safety, fn func(context.Context, In) (Out, error)) Tool
Func wraps a typed Go function into a Tool. In is JSON-decoded from the args; the return value is JSON-encoded. This is the compile-time-typed ergonomic: change In and the handler won't compile. The Tool interface itself stays untyped so a map of mixed tools (and runtime MCP tools) works.
func RetrievalTool ¶
RetrievalTool exposes a Retriever as a tool the model can call to search on demand (agentic RAG): the model decides when to retrieve and with what query. It returns the top-k documents. Read-only (retry-safe).
Example ¶
ExampleRetrievalTool shows bring-your-own-RAG: implement Retriever against your store, wire it in as a tool (or with WithRetrieval), and the loop handles the rest.
// Your store, behind the Retriever port (here a trivial in-memory one).
docs := map[string]string{"france": "Paris is the capital of France."}
r := retrieverFunc(func(_ context.Context, query string, _ int) ([]Doc, error) {
if d, ok := docs[strings.ToLower(query)]; ok {
return []Doc{{Text: d}}, nil
}
return nil, nil
})
tool := RetrievalTool(r, 1)
out, _ := tool.Call(context.Background(), json.RawMessage(`{"query":"france"}`))
fmt.Println(string(out))
Output: [{"text":"Paris is the capital of France."}]
func SubAgent ¶
SubAgent wraps an agent as a Tool so a parent can delegate to it — the multi-agent primitive, as a durable tree rather than a fragile handoff.
The sub-run journals under a hierarchical, deterministic ID (parentRunID/toolUseID) — so give `sub` the SAME Durable store as the parent for a unified journal. Then a crash ANYWHERE in the tree resumes the whole tree precisely: completed sub-agents are reused, the in-flight one resumes from its own journal, and ResumeHalt / PendingApproval from deep in the tree propagate up (approve, re-run the root, and it resumes down the path).
This is what the incumbents can't do: ADK/agenticenv can't recover sub-agents across a restart, and Eino doesn't unify nested state into the parent checkpoint.
type ToolCallDelta ¶
type ToolCallDelta struct {
Index int
ID string
Name string
ArgsFragment json.RawMessage
}
ToolCallDelta is an incremental fragment of a tool call, keyed by Index. Fragments are concatenated and gated by json.Valid before use (the UTF-8/partial-JSON safety the research flagged as universally missing).
type ToolChoice ¶
type ToolChoice struct {
Mode string // "", "auto", "none", "required", or "tool"
Name string // tool name to force; only meaningful when Mode == "tool"
}
ToolChoice controls whether and how the model may call tools on a request. Mode is one of "" or "auto" (model decides, the default), "none" (never call a tool), "required" (must call some tool), or "tool" (must call the specific tool named in Name). A nil *ToolChoice on a Request means the provider default (auto).
CAVEAT: in the multi-turn agent loop, forcing "required" or "tool" on EVERY turn prevents the model from ever emitting a final text answer, so the loop never terminates. Those two modes are intended for single-turn or typed/structured calls where exactly one tool round-trip is expected. Use "auto" (the default) for the loop.
type ToolCompleted ¶
type ToolCompleted struct {
ToolUseID string
Name string
Result json.RawMessage
IsError bool
}
ToolCompleted carries a tool call's result. Emitted for live executions, for a human-denied call, and for each result reconstructed from the journal on resume.
type ToolHandler ¶
ToolHandler executes one tool call: given the ToolUse (ID, Name, Args), it returns the raw JSON result or an error. It is the tool-side analogue of ModelHandler.
type ToolMiddleware ¶
type ToolMiddleware func(ToolHandler) ToolHandler
ToolMiddleware wraps a ToolHandler — the same func(Handler) Handler idiom as model Middleware, but around TOOL execution. First added = outermost. A middleware can:
- observe/log/trace a call (its name, args, duration, error);
- MUTATE the outgoing args (validate, redact, inject defaults) by calling next with a modified ToolUse;
- transform the result before it is journaled;
- SHORT-CIRCUIT — return a result (a cache hit) or an error (a policy denial) WITHOUT calling next, so the tool never runs.
The chain runs INSIDE the durable, memoized step, so a short-circuit result or a transformed result is what gets journaled — resume replays it and never re-runs the middleware or the tool. Batteries live in the middleware/ package (ToolLog, ToolCache).
type ToolResult ¶
type ToolResult struct {
ToolUseID string `json:"tool_use_id"`
Result json.RawMessage `json:"result,omitempty"`
IsError bool `json:"is_error,omitempty"`
}
ToolResult carries a tool's output back to the model (tool turn).
type ToolResultCodec ¶ added in v0.2.0
type ToolResultCodec interface {
EncodeToolResult(raw json.RawMessage) (string, error)
}
ToolResultCodec renders a tool result's canonical JSON bytes into the string content that is sent to the model. The journal always stores the JSON form (see Record.Result), so this only changes what the model reads: an adapter can hand the model a more token-efficient or better-comprehended encoding (for example GCF via github.com/bide-ai/bide/codec/gcf) without touching the durable record or the audit trail. Implementations must be pure and deterministic.
type ToolStarted ¶
type ToolStarted struct {
ToolUseID string
Name string
Args json.RawMessage
}
ToolStarted fires when a tool call begins executing (after any approval gate and, for non-idempotent tools, after the durable attempt marker is written).
type ToolUse ¶
type ToolUse struct {
ID string `json:"id"`
Name string `json:"name"`
Args json.RawMessage `json:"args,omitempty"`
}
ToolUse is the model's request to call a tool (assistant turn).
type TurnStarted ¶
type TurnStarted struct{ Seq int }
TurnStarted marks the beginning of a fresh model turn (Seq is the model-call sequence number within the run). Not emitted for turns replayed from the journal.
type Usage ¶
type Usage struct {
InputTokens int
OutputTokens int
// CacheReadTokens is input tokens served from the provider's prompt cache (billed at
// a discount). CacheWriteTokens is input tokens written to the cache on this call
// (Anthropic cache creation; 0 for providers that cache implicitly, like OpenAI).
CacheReadTokens int
CacheWriteTokens int
}
Usage is token accounting for a call; middleware turns it into cost.
type Waker ¶
type Waker interface {
// Schedule registers that runID should be resumed at fireAt, idempotent per (runID, name).
Schedule(runID, name string, fireAt time.Time)
}
Waker is the pluggable trigger that re-invokes a sleeping run when its durable timer is due. Sleep registers a wake with the Waker bound to the run's context (WithWaker); the Waker later calls back to resume the run. The SDK provides the durable, at-most-once timer and its resume safety; what re-invokes the run at the wake time is deployment policy (an in-process loop, a cron, a queue), exactly as the inbound trigger for an event-driven run is (see docs/MESSAGING.md). MemWaker is the reference in-process implementation.