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 ClassifyStreamError(provider string, data []byte) error
- func DetachModelSink(ctx context.Context) (context.Context, func(Event))
- func EmitMessage(sink func(Event), m Message)
- func EncodeRecord(r Record) ([]byte, error)
- 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 JournalEntry(name string, rec Record) ([]byte, error)
- func Lease(ctx context.Context, store Durable, runID string, ...) (bool, error)
- func NewSSEScanner(body io.Reader) *bufio.Scanner
- func NextOnceKey(ctx context.Context) string
- 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 SSEReadError(provider string, err error) error
- 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 WithToolSafety(ctx context.Context, s Safety) 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) WithTokenBudget(max int) *Agent
- func (a *Agent) WithToolChoice(tc ToolChoice) *Agent
- func (a *Agent) WithToolErrorRedactor(fn func(tool string, err error) string) *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 StepOption
- 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.
const MaxSSELine = 32 << 20
MaxSSELine is the longest SSE line NewSSEScanner reads. It is sized for the largest single event a model streams: a big tool-call arguments blob, or an image Gemini returns inline as base64, which runs to several megabytes.
const SaltSize = 32
SaltSize is the length of Record.Salt: 32 bytes (256 bits) from crypto/rand.
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) // ErrToolReinvoked is a tool middleware calling a tool that is not retry-safe a second // time for the same tool call. The agent refuses and does not run the tool again. ErrToolReinvoked = fmt.Errorf("tool invoked again for one call: %w", ErrConfig) ErrNoRecordedOutput = fmt.Errorf("no recorded model output: %w", ErrModel) // ErrIncompleteResponse is a model stream that ended without a Finish event: the // response stopped partway through a turn, so what arrived is not the model's answer. ErrIncompleteResponse = fmt.Errorf("model response ended before the turn finished: %w", ErrModel) ErrTruncatedToolArgs = fmt.Errorf("truncated tool-call arguments: %w", ErrProtocol) // ErrStreamProtocol is a model stream that broke its provider's event protocol, such as // content after the turn's end or an event after the Finish. What arrived is not a turn the // model finished, so it is not the answer. It wraps ErrModel as well as ErrProtocol: the // turn is not journaled and a fresh attempt can succeed, so a retry middleware retries it. ErrStreamProtocol = fmt.Errorf("model stream broke its protocol: %w (%w)", ErrProtocol, ErrModel) // ErrToolUseIDReused is a model turn whose tool call has no ID, repeats an ID from an // earlier turn of the conversation, or repeats one within the turn. The loop keys each // call's result and journal step by its ID, so a reused ID would pass the call off as // already done. It wraps ErrModel as well as ErrProtocol: the fault is in the model's // output, the turn is not journaled, and a fresh attempt can issue a valid turn, so a // retry middleware treats it as retryable. ErrToolUseIDReused = fmt.Errorf("tool-use id missing or reused: %w (%w)", ErrProtocol, ErrModel) // ErrQuotaExhausted is a provider refusing a call because the account's quota or credit // is used up (OpenAI insufficient_quota, Anthropic billing_error, HTTP 402, a Gemini daily // quota), as opposed to a momentary rate limit. Waiting a few seconds does not lift it, so // middleware.Retryable does not retry it. It arrives as an *APIError. ErrQuotaExhausted = fmt.Errorf("provider quota or credit exhausted: %w", ErrModel) // ErrResponseTooLarge is a streamed response with a line longer than MaxSSELine. The same // request would produce it again, so middleware.Retryable does not retry it. ErrResponseTooLarge = fmt.Errorf("model response line too large: %w", ErrModel) 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 for the top-level run 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.
The outcome is journaled too, at-most-once by name, the first time either side wins. Every later evaluation of the same await (the tool re-runs because it pauses afterwards, or the process dies before the tool's result is recorded) returns that recorded outcome, so a signal delivered after the timeout won cannot flip the call to the signal branch.
func ClassifyHTTPError ¶
ClassifyHTTPError turns a non-2xx model response into the appropriate SDK error. It reads at most 64KB of the body and parses the provider's error object ({"error":{...}} from OpenAI, Anthropic, and Gemini) so the result carries the provider's message, type, and code, each (and the Body) cut to 8KB:
- The account's quota or credit is used up (HTTP 402; an error type or code of insufficient_quota, billing_hard_limit_reached, billing_not_active, or billing_error; a Gemini quota violation whose quotaId is per day): an *APIError wrapping ErrQuotaExhausted, whatever the status, since no wait lifts it.
- HTTP 429 otherwise: a *RateLimited whose RetryAfter is the Retry-After header or, without one, Gemini's RetryInfo.retryDelay.
- Anything else: an *APIError with the status code.
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 ClassifyStreamError ¶ added in v0.7.0
ClassifyStreamError turns an error object a provider sends inside a 200 stream (an OpenAI or Gemini data line holding {"error":{...}}, an Anthropic "error" event) into an SDK error. With a numeric code (Gemini sends the HTTP status) it is classified as ClassifyHTTPError would a response with that status. Without one, an exhausted quota is an *APIError wrapping ErrQuotaExhausted, a rate limit (type or code rate_limit_error or rate_limit_exceeded) is a *RateLimited, and anything else (overloaded, server_error) is an ErrModel carrying the provider's message, which middleware.Retryable retries.
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 EncodeRecord ¶ added in v0.7.0
EncodeRecord returns the journal encoding of r: the bytes a store persists for it and the bytes an audit leaf commits to. Every store writes exactly these bytes, so a record's journal form does not depend on the backend, and every store hands back DecodeRecord of them, on the live path as on replay, so a resumed run rebuilds exactly the conversation the live run had.
The encoding is JSON with two properties that make it canonical:
- It is a fixed point: decoding it with DecodeRecord and encoding the result again yields the same bytes. So a store holds exactly EncodeRecord(rec) for the record rec it hands back, and an audit leaf computed from a record read back from any store is the bytes that store persisted.
- It keeps the content of every json.RawMessage (a tool's arguments and result, a step's value, a reconciler's evidence) byte for byte, except whitespace between JSON tokens, which is removed. Nothing is HTML-escaped: a tool result that says "<b>" is journaled as "<b>", not with the escapes json.Marshal writes for < and >, so an adapter that forwards the JSON text to a model sends what the tool returned. Key order, number spelling ("1.50", "1e20"), escapes already in the text, and even invalid UTF-8 inside a JSON string are kept as they are.
Insignificant whitespace is dropped because encoding/json compacts every raw value it embeds, and one spelling per JSON value is what an audit leaf should commit to. For the same reason the line and paragraph separators U+2028 and U+2029 are always written as their JSON escapes, which encoding/json emits in some builds but not others. In a Go string field (message text, a reasoning signature), invalid UTF-8 becomes U+FFFD, as encoding/json writes it; since the stores hand out only the decoded form, the live and replayed conversations agree.
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 JournalEntry ¶ added in v0.7.0
JournalEntry returns the bytes a store persists when it records rec as the step named name: rec with Name set to name and a fresh random Salt (see Record.Salt), in its journal encoding (EncodeRecord). Every Durable implementation must record a new step through it, so every record carries a salt; the audit package refuses to commit a record without one. It errors only if the system's random source fails or rec cannot be encoded.
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 MaxSSELine. A single SSE line can carry a large tool-call arguments blob or inline image, and the 64KB default would fail the stream on it (the openai-go #368 lesson). The buffer grows only as a line needs it. Every adapter reads its event stream through this so the cap is fixed in exactly one place; pass the scanner's Err to SSEReadError.
func NextOnceKey ¶ added in v0.7.0
NextOnceKey returns a key for the next exactly-once operation (an idempotent append, say) that the tool call running in ctx makes, or "" outside a tool call. The call's operations are numbered in the order they ask for keys, and the numbering starts again each time the call runs, so an operation keeps its key when the call runs again after a crash, and two operations of one call never share one.
The numbering is stable only if the call asks for its keys in the same order each time it runs. Work a call does concurrently must therefore run in steps (Step, or Parallel's tasks): each step numbers its own operations, under a key that names the step, so concurrent steps keep their keys whatever order they run in.
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, 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. Succeeded tasks are memoized on a later resume. A failed task was not journaled: one marked retry-safe (Task.Safety) re-runs, and a side effect halts with *ResumeHalt, as Step does, since a failed effect may still have landed. Task names must be unique; a repeated name is ErrConfig. 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 halt's RootRunID, which is the same run unless the halt came from inside a sub-agent:
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.RootRunID, 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 first final_answer call the tool accepts ends the run: the model is not asked for another turn.
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. Only if the model never makes an accepted final_answer call (it answers in plain text instead) does RunTyped parse its last 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 SSEReadError ¶ added in v0.7.0
SSEReadError wraps a failed read of a model's SSE stream (the Err of a NewSSEScanner scanner) as an ErrModel. A line over MaxSSELine is ErrResponseTooLarge, which the same request would hit again, so middleware.Retryable does not retry it; any other read failure (a connection cut partway through) is retried.
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), opts ...StepOption) (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", fetchInvoice, agent.StepSafety(agent.Safety{ReadOnly: true}))
res, err := agent.Step(ctx, dur, runID, "reserve", reserve) // a side effect: at most once
A step runs at most once, like a tool call. By default it is treated as a side effect: an attempt marker is journaled before fn runs, so if the process dies after fn's effect and before its result is recorded, the resumed step returns *ResumeHalt instead of running fn again. Clear it with ResolveHalt (the halt's ToolUseID is the step name) once the true outcome is known. A step that is safe to re-run declares it with StepSafety (ReadOnly, Idempotent, or an IdempotencyKey); it then skips the marker and simply re-runs after a crash.
If fn returns an error, nothing is recorded but the marker: a side-effecting step whose fn failed halts on the next attempt too, since a failed call may still have taken effect.
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.
func WithToolSafety ¶ added in v0.6.0
WithToolSafety returns ctx carrying s as the Safety that ToolSafety reports. The agent sets it for every tool call; call it yourself to run tool middleware outside an agent, as in a test. It informs middleware only: the agent's at-most-once check reads the registered tool's Safety, never this value.
Types ¶
type APIError ¶
type APIError struct {
StatusCode int
Body string
Message string // the provider's error message
Type string // the provider's error type (OpenAI, Anthropic) or status (Gemini)
Code string // the provider's error code (OpenAI)
Err error
}
APIError is returned by a provider adapter for a non-2xx HTTP response that is not a rate limit (which uses RateLimited), and for an exhausted quota whatever its status (wrapping ErrQuotaExhausted). It carries the StatusCode so a retry classifier can tell a transient failure (5xx, 408) from a terminal one (most 4xx: auth, validation), the provider's error fields when the body held its error object, and the Body (cut to 8KB) for diagnostics. It wraps Err (typically ErrModel) so errors.Is(err, ErrModel) holds. StatusCode is 0 for an error the provider sent partway through a stream without one.
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 a durable memoized step: once recorded it never runs again, and a crash mid-compensation re-runs it (so Compensate must be idempotent).
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) WithTokenBudget ¶ added in v0.6.0
WithTokenBudget caps the tokens a single run may use: once the run's model calls have used max tokens or more (Usage.TotalTokens, cached input included), the run makes no further model call and returns ErrBudgetExceeded (category ErrBudget). A call's usage is known only after it returns, so the call that crosses max completes; the next one is refused. max <= 0 means unbounded (the default). Returns the agent for chaining.
The budget is per run and durable: each model call's usage is journaled with its turn, and the count is rebuilt from the journal, so a resumed run, on any process, is held to what it has already used. Like WithMaxTurns it applies per run (per Session.Send turn), and a sub-agent's run has its own budget.
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.
func (*Agent) WithToolErrorRedactor ¶ added in v0.7.0
WithToolErrorRedactor sets the text recorded for a tool call that fails. A failed call's error text is journaled as the call's result, sent to the model, and hashed into the audit trail, where a proof can disclose it, so it must not carry secrets. By default the text is the error's own, with every URL in it redacted: its userinfo, each query parameter's value, and its fragment become REDACTED (a net/http *url.Error quotes the whole request URL, API key parameter included). fn receives the tool's name and its error and returns the text to record instead; the same URL redaction then applies to what fn returns. Use it to scrub what URL redaction cannot know about (an account number, a token in a header echoed back), and keep the text useful: the model reads it to decide what to do next.
It applies to every failed call, a sub-agent's included (a sub-agent's failure is its tool call's error), and to the failure a saga journals. The error the caller gets back (SagaAborted.Cause, for one) is the tool's own. Returns the agent for chaining.
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 ProjectEvents ¶ added in v0.7.0
func ProjectEvents(recs []Record) (events []AgentEvent, sources []int)
ProjectEvents returns the events ReplayEvents returns for a journal holding recs, and for each event the index in recs of the record it projects: each event comes from exactly one record. The audit package uses the index to salt a projected event from its record's salt.
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
// RootRunID is the run to re-invoke to continue: the top-level run. It differs from RunID
// when the signal comes from inside a sub-agent, whose journal is RunID. Record the answer
// against RunID (Resume, Approve, ResolveHalt, Signal), then run RootRunID with the root agent.
RootRunID 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 a fresh Salt: persist
// the bytes JournalEntry returns), 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
// RootRunID is the run to re-invoke to continue: the top-level run. It differs from RunID
// when the signal comes from inside a sub-agent, whose journal is RunID. Record the answer
// against RunID (Resume, Approve, ResolveHalt, Signal), then run RootRunID with the root agent.
RootRunID 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 journal encoding (EncodeRecord) 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 Do hands back the record a replay reads, on the live path too, 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.
A resume that fails (the model provider or the store was briefly unavailable) leaves the run where it was, so its due timers are put back and the next Fire retries it; without that, one transient failure would leave the run asleep with nothing to wake it. A resume that ends in a durable pause (the run slept again, or awaits approval, an interrupt, a signal, or a halt resolution) is not retried: the run is waiting on something else, and waking it every tick would only spin.
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.
func (*MemWaker) Start ¶
func (w *MemWaker) Start(ctx context.Context, every time.Duration, onError func(error)) <-chan struct{}
Start runs Fire on a ticker every `every` until ctx is cancelled, so sleeping runs wake on their own. Resume errors go to onError if non-nil. It is the turnkey local loop; a production deployment may prefer an external scheduler that owns the trigger and durable timer set.
The returned channel closes once the loop has stopped, after any Fire in flight when ctx was cancelled has returned. Wait on it before closing the store the resumed runs write to.
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, RateLimit, Cost, ...).
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
// RootRunID is the run to re-invoke to continue: the top-level run. It differs from RunID
// when the signal comes from inside a sub-agent, whose journal is RunID. Record the answer
// against RunID (Resume, Approve, ResolveHalt, Signal), then run RootRunID with the root agent.
RootRunID 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 ¶
type RateLimited struct {
RetryAfter time.Duration
Message string // the provider's error message, if it sent one
Err error
}
RateLimited is returned by a provider adapter when it is rate limited (HTTP 429 that is not an exhausted quota; see ClassifyHTTPError). It carries an optional RetryAfter hint from the Retry-After response header or the provider's own retry delay (0 means no hint was provided), and the provider's error message. 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 ¶
type Reasoning struct {
Text string `json:"text"`
Signature string `json:"signature,omitempty"`
Redacted string `json:"redacted,omitempty"`
}
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.
Redacted holds reasoning the provider returned encrypted (Anthropic redacted_thinking data), which must also go back unchanged; Text and Signature are empty when it is set. Each thinking block is its own Reasoning part, in the order the model produced them.
type ReasoningDelta ¶
type ReasoningDelta struct {
Text string
Signature string // opaque provider token to echo back on later turns (Anthropic thinking)
Redacted string // encrypted reasoning to echo back unchanged (Anthropic redacted_thinking)
}
ReasoningDelta is a fragment of model thinking. A Signature ends the thinking block it belongs to, so the next ReasoningDelta starts a new Reasoning part. Redacted carries a whole encrypted block (Anthropic redacted_thinking), which becomes a Reasoning part of its own.
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
Usage *Usage `json:"usage,omitempty"` // StepModel: the call's token usage
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"`
// Salt is SaltSize random bytes a store sets when it first journals the record (see
// JournalEntry), replacing any salt the step returned. It is persisted with the record and
// read back verbatim, so it is stable across replay and across stores. It has no meaning to
// the run; the audit trail needs it. An audit leaf commits to the record's journal encoding,
// salt included, and an inclusion proof for one record carries its neighbours' leaf hashes, so
// without the salt anyone holding a proof could confirm a guessed neighbour (an approval, a
// small tool result) by hashing it. The salt is disclosed only with its own record.
Salt []byte `json:"salt,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.
func DecodeRecord ¶ added in v0.7.0
DecodeRecord decodes a record from its journal encoding (see EncodeRecord) into an independent copy that shares no memory with b.
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
// RootRunID is the run to re-invoke to continue: the top-level run. It differs from RunID
// when the signal comes from inside a sub-agent, whose journal is RunID. Record the answer
// against RunID (Resume, Approve, ResolveHalt, Signal), then run RootRunID with the root agent.
RootRunID 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. For a trusted MCP server, its tool annotations (readOnlyHint / idempotentHint) map onto it (see mcp.TrustAnnotations).
func ToolSafety ¶ added in v0.6.0
ToolSafety returns the Safety of the tool that a tool-middleware call is for, so a middleware can tell a side effect from a read before it retries, caches, or skips a call. ok is false outside a tool call, or when the call names no registered tool; a middleware should then treat the call as a side effect.
func (Safety) RetrySafe ¶ added in v0.6.0
RetrySafe reports whether a call to the tool may run more than once for one tool call: it is ReadOnly, Idempotent, or carries an IdempotencyKey. It is the test the agent applies on resume, and the one tool middleware applies before retrying a call (see ToolSafety).
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: a turn runs under its own run ID ("<id>/tN" for Send, "<id>/e/<key>" for SendOnce), whose durable journal handles crash resume WITHIN the turn; the session-level journal under "<id>" records which message started each Send turn and 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.
A turn belongs to the message that started it: while a Send turn is unfinished, Send with a different message is ErrConfig rather than resuming that turn. Several handles on one session (a stale handle, or two workers) never lose a turn or answer one message with another's reply; a handle that finds the journal moved on reloads it.
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) or fails, 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. Until then, Send with a different input is ErrConfig: the open turn belongs to its message.
func (*Session) SendOnce ¶ added in v0.6.0
SendOnce runs one conversation turn for an inbound message identified by key (an event or message id), at most once per key. A key whose turn already completed returns that turn's answer without running anything, so a redelivered message never opens a second turn, even if the process died after the turn was recorded and before the caller replied. A key whose turn was interrupted resumes that same turn: it runs under its own journal, "<session id>/e/<key>", so a different message arriving in between gets its own turn. Reusing a key with a different input is ErrConfig.
type Sleeping ¶
type Sleeping struct {
RunID string
// RootRunID is the run to re-invoke to continue: the top-level run. It differs from RunID
// when the signal comes from inside a sub-agent, whose journal is RunID. Record the answer
// against RunID (Resume, Approve, ResolveHalt, Signal), then run RootRunID with the root agent.
RootRunID string
Name string
FireAt time.Time
}
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 StepOption ¶ added in v0.6.0
type StepOption func(*stepConfig)
StepOption configures Step.
func StepSafety ¶ added in v0.6.0
func StepSafety(s Safety) StepOption
StepSafety declares how safe a step is to re-run, as Safety does for a tool. A step that is RetrySafe (ReadOnly, Idempotent, or keyed) re-runs after a crash; any other step halts.
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).
A consumer that stops early releases the producer: breaking out of Events() does it, and so does Close. Cancelling the context passed to Model.Stream also releases it.
func NewStream ¶
NewStream wraps an event channel the caller fills and closes. It suits producers that buffer their whole response up front. A producer that sends as it reads (a network adapter) should use NewStreamFunc, so it is released when the consumer stops reading; a goroutine blocked sending on ch is not.
func NewStreamFunc ¶ added in v0.6.0
NewStreamFunc runs produce in its own goroutine and returns the Stream it feeds. produce delivers each event through send, which blocks until the consumer takes it and returns false once the consumer has stopped reading (it broke out of Events or called Close) or ctx is cancelled; produce should then return promptly, releasing whatever it holds, such as a response body. The stream ends when produce returns.
If ctx cancels an event the consumer has not yet taken, the stream ends with ctx's error, so a consumer never mistakes a response cut short by cancellation for a complete one.
func (*Stream) Close ¶ added in v0.6.0
func (s *Stream) Close()
Close tells the producer the consumer will read no further events, so it can stop and release its resources. It is safe to call more than once, and after the stream ends.
func (*Stream) Events ¶
Events returns a range-over-func iterator (Go 1.23+) over streamed events. Breaking out of the loop closes the stream. A stream that ends without a Finish event yields ErrIncompleteResponse last: the response stopped partway through the turn. A Finish is the turn's last event: an event after it yields ErrStreamProtocol and ends the stream.
type Task ¶
type Task[T any] struct { Name string Fn func(context.Context) (T, error) // Safety declares whether the task may re-run after a crash, as StepSafety does for a Step. // The zero value treats the task as a side effect: it runs at most once, and a crash after // its effect halts the resumed call with *ResumeHalt. Mark a check or a lookup ReadOnly. Safety Safety }
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 panics, as New does for a missing model, if schema.For cannot describe In: such a type (a field reached through an embedded pointer to an unexported struct) could never be decoded from a call's arguments, so the tool would fail every call.
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
Signature string // opaque provider token for this call, kept on ToolUse.Signature (Gemini thoughtSignature)
}
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.
A middleware that may call next more than once (a retry) or not at all (a cache) must check the tool's Safety first, with ToolSafety(ctx): repeating or skipping a side effect is not its call to make. The agent enforces the first half itself: a tool that is not RetrySafe runs at most once per tool call, and a second call to next for it returns ErrToolReinvoked without running the tool.
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"`
Signature string `json:"signature,omitempty"`
}
ToolUse is the model's request to call a tool (assistant turn). Signature is an opaque provider token bound to this call that must be sent back with it on later turns (a Gemini thoughtSignature); it is journaled with the call, and adapters that have no such token leave it empty and ignore it.
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 is input tokens neither read from nor written to the prompt cache.
InputTokens int `json:"input_tokens,omitempty"`
OutputTokens int `json:"output_tokens,omitempty"`
// 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 `json:"cache_read_tokens,omitempty"`
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
}
Usage is token accounting for a call; middleware turns it into cost. The four counts are disjoint, whatever the provider: each input token is counted in exactly one of InputTokens, CacheReadTokens, or CacheWriteTokens, so each is billed once at its own rate and TotalInputTokens is the whole prompt. Providers differ on the wire (Anthropic reports uncached input separately; OpenAI and Gemini report a prompt total that includes cached tokens), and adapters normalize to this form.
func (Usage) TotalInputTokens ¶ added in v0.6.0
TotalInputTokens is every input token the call processed, cached or not.
func (Usage) TotalTokens ¶ added in v0.6.0
TotalTokens is every token the call processed: all input plus output.
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/guides/messaging.md). MemWaker is the reference in-process implementation.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package durabletest checks that an implementation satisfies the agent.Durable contract's fidelity guarantee, so every store (in-memory, SQLite, Postgres, or your own) is held to the property the agent loop's resume relies on: the record Do returns on the live path is exactly the record a replay reads back, for any content a model or tool can produce.
|
Package durabletest checks that an implementation satisfies the agent.Durable contract's fidelity guarantee, so every store (in-memory, SQLite, Postgres, or your own) is held to the property the agent loop's resume relies on: the record Do returns on the live path is exactly the record a replay reads back, for any content a model or tool can produce. |