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 Capability[T any](store Durable) (T, bool)
- func DetachModelSink(ctx context.Context) (context.Context, func(Event))
- func EmitMessage(sink func(Event), m Message, u Usage)
- func EncodeRecord(r Record) ([]byte, error)
- 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 IsReservedStepName(name string) bool
- func IsSessionRun(runID string) bool
- func IsSubRun(runID string) bool
- func JournalEntry(name string, rec Record) ([]byte, error)
- func Lease(ctx context.Context, store Durable, runID string, ...) (bool, error)
- func NextOnceKey(ctx context.Context) string
- func Parallel[T any](ctx context.Context, d Durable, runID string, maxConcurrency int, ...) ([]T, error)
- func Recover(ctx context.Context, store Durable, ...) (int, error)
- func RecoverLoop(ctx context.Context, store Durable, ...) error
- func RedactURLs(s string) string
- 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 ResolveStepHalt(ctx context.Context, store Durable, runID, name 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 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 SubRunID(parent, toolUseID string) string
- func TallyApprovals(recs []Record, s ApprovalSubject, p ApprovalPolicy, ...) (ApprovalTally, []DecisionCheck)
- func ToolErrorText(ctx context.Context, tool string, err error) string
- func ToolResultStep(toolUseID string) string
- 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 WithModel(ctx context.Context, m Model) (_ context.Context, ok bool)
- func WithModelCallHook(ctx context.Context, h ModelCallHook) (_ context.Context, ok bool)
- 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 Describer
- type Doc
- type Durable
- type Emit
- type Event
- type Finish
- type FinishReason
- type Finished
- type HaltTooYoung
- type Identity
- type Image
- type Interrupted
- 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 ModelCallHook
- type ModelEvent
- type ModelHandler
- type ModelInfo
- 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 RetrievalOption
- type Retriever
- type Role
- type RunStart
- 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 ToolStarted
- type ToolUse
- type TurnRestarted
- 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 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) // ErrToolOutcomeUnknown is a tool call that may or may not have taken effect: the tool // sent its request but lost the connection (or its deadline passed) before the answer // came back. A tool returns an error wrapping it instead of an ordinary failure when it // cannot tell. For a tool that is not retry-safe the agent records no result, as it does // for a cancelled call: the run stops with this error, and a resume halts (*ResumeHalt) // rather than run the side effect a second time. For a retry-safe tool the call is an // ordinary failure the model sees, since running it again does no harm. ErrToolOutcomeUnknown = fmt.Errorf("tool outcome unknown: %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) // ErrOutputTruncated is a model turn that ended at its output token limit (or the context // window): Finish reason FinishLength. What arrived is only the start of the model's answer, // so it is not journaled. It wraps ErrModel, so middleware.Retryable retries it; the same // request usually ends the same way, so raise the limit (MaxTokens) rather than rely on a retry. ErrOutputTruncated = fmt.Errorf("model output cut off at its token limit: %w", ErrModel) // ErrOutputFiltered is a model turn that a provider's safety or content filter stopped, or // that the model refused: Finish reason FinishFiltered. What arrived is not the model's whole // answer, so it is not journaled. It wraps ErrModel, so middleware.Retryable retries it. ErrOutputFiltered = fmt.Errorf("model output stopped by a content filter or refusal: %w", ErrModel) // 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, repeats one within the turn, or has an ID that is not // valid UTF-8 (the journal cannot hold it). 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. Any other ID is // safe: the keys derived from it encode it (see ToolResultStep and SubRunID). 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, reused, or malformed: %w (%w)", ErrProtocol, ErrModel) // ErrNegativeUsage is token usage with a negative count, reported by a model or returned by // a middleware. The usage is rejected rather than lowering a run's totals and budget. It // wraps ErrModel as well as ErrProtocol. ErrNegativeUsage = fmt.Errorf("negative token usage: %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 the adapter reads, or // longer in all than its adapter's cap (see MaxSSELine and LimitResponse in model/provider). 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.
var ErrLeaseLost = errors.New("run lease lost")
ErrLeaseLost ends a drive whose run lease was lost while it ran: the store reported the lease no longer held (it lapsed and another process took it), or no renewal succeeded before the renewal cutoff (see Lease). The drive's context is cancelled with it as the cause (context.Cause), and Lease returns the drive's error wrapped with it. It is a coordination signal, not a fault, so like context.Canceled it wraps no category: the run is not failed, it is another driver's to continue. Recover does not count it as a failure.
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 Capability ¶ added in v0.8.0
Capability returns store's implementation of the optional capability T (Lister, Leaser, or any other interface a store may implement beyond Durable), looking through wrappers.
A store that wraps another Durable (an audit or tracing layer, say) exposes the store it wraps by implementing
Unwrap() Durable
Capability checks store itself first, then follows Unwrap until a store implements T, and reports false once a store has no Unwrap method or Unwrap returns nil. A wrapper therefore exposes exactly the capabilities of the store it wraps, with no forwarding methods to keep in step with new capabilities. A wrapper that must change what a capability means (one that renames runs, for example, and so must rename what Runs returns) implements that capability itself, which takes precedence over the wrapped store's, as with errors.As. Lease, Recover and RecoverLoop find Leaser and Lister this way; code that needs a capability of a store it did not build should too, since a plain type assertion does not see through a wrapper.
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 a model call's response, m and its usage u, to sink as the events a model would have streamed for it, ending with a Finish that carries u. 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 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 IsReservedStepName ¶ added in v0.8.0
IsReservedStepName reports whether name starts with a prefix the engine reserves for its own journal keys. Step, Parallel and ResolveStepHalt refuse such a name.
func IsSessionRun ¶ added in v0.8.0
IsSessionRun reports whether runID is a session's journal or one of its turn runs, which the session drives (see Session). A sub-agent called in a turn is IsSubRun instead.
func IsSubRun ¶ added in v0.8.0
IsSubRun reports whether runID is a sub-agent's run (SubRunID), which its root run drives: its last '>' is followed by an encoded call ID, not by a session's mark.
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 neither the store nor a store it wraps implements Leaser (see Capability), drive runs unconditionally. The bool reports whether drive ran; the error is the acquisition error (when false) or drive's own error (when true).
If the lease is lost while drive runs (see driveWithRenew for the renewal schedule), drive's context is cancelled with ErrLeaseLost as its cause (context.Cause), and a drive that then returns an error has it wrapped with ErrLeaseLost, so errors.Is(err, ErrLeaseLost) tells a lost lease from a shutdown or a genuine failure. A drive that returns nil succeeded regardless.
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 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 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, in one pass. 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 the next pass of another process takes the run over: Recover is one pass, so run RecoverLoop to keep passing. 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. A run whose lease was lost mid-drive (ErrLeaseLost) is not joined either: another process holds it now and carries it on.
resume is deployment POLICY, not a mechanism the SDK can supply: it knows which agent drives a run 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). The run's input and entry point (Run or RunSaga) are in its journal (see RecordedStart), and a resume with another input or entry point is ErrConfig. A typical resume is:
func(ctx context.Context, runID string) error {
start, ok, err := agent.RecordedStart(ctx, store, runID)
if err != nil {
return err
}
if !ok {
start = startFor(runID) // your own record, for a run not driven under this version
}
ctx = agent.WithWaker(ctx, w)
if start.Saga {
_, err = a.RunSaga(ctx, runID, start.Input)
} else {
_, err = a.Run(ctx, runID, start.Input)
}
return err
}
Recover skips a sub-agent's run (IsSubRun): its root run drives it, and re-running the root resumes it. It skips a session's journal and turn runs (IsSessionRun) too: a turn is seeded with the transcript before its message, which only the session holds, and only the session records its answer, so an unfinished turn resumes when its message is sent again (Send with the same input, or the redelivered SendOnce). resume should no-op any other runID it does not own; Recover re-drives every other incomplete run it enumerates.
func RecoverLoop ¶ added in v0.8.0
func RecoverLoop(ctx context.Context, store Durable, resume func(ctx context.Context, runID string) error, opts ...RecoverOption) error
RecoverLoop re-drives in-flight runs until ctx is done, so a run whose holder dies is taken over automatically: Recover is a single pass, and a run another holder leases at that moment is left for a later one. Each pass, started every WithRecoverInterval (half the lease TTL by default), enumerates the store's runs as Recover does and drives each incomplete run it can lease, so a dead holder's run is picked up within about one interval of its lease expiring (the TTL after the holder's last renewal).
Runs are driven concurrently, up to WithRecoverConcurrency at once (16 by default), so one long drive does not hold up the others; a run this loop is already driving is not started again. A pass that finds every slot busy waits for one, so each pass reaches every run it listed; the next pass starts when this one has started all of its drives and the interval has elapsed. Genuine failures go to the WithRecoverErrors handler, and the run is retried on the next pass; pauses and lost leases are not failures (see Recover).
Run it once per process, for the life of the process, with the same resume Recover takes:
go func() {
err := agent.RecoverLoop(ctx, store, resume,
agent.WithLeaseHolder("worker-1"),
agent.WithRecoverErrors(func(err error) { log.Print(err) }))
// err is ctx's error once ctx is done
}()
A configuration error (a store that does not implement Lister, a non-positive TTL, interval or concurrency) is returned at once. Otherwise RecoverLoop returns ctx's error when ctx is done, after the drives it started (whose contexts derive from ctx) have returned.
func RedactURLs ¶ added in v0.8.0
RedactURLs returns s with every URL in it redacted: its userinfo, each query parameter's value, and its fragment become REDACTED, and its scheme, host, path, and query parameter names are kept. It is the redaction the agent applies to a failed tool call's text; use ToolErrorText for a tool error, and this for other error text that can quote a URL.
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 on a tool call. 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 (under ToolResultStep, the 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. A halted Step (its ResumeHalt has no ToolName) is cleared with ResolveStepHalt: ResolveHalt refuses an ID that only a Step has attempted. In a saga (RunSaga), a failure recorded this way is a failed step: the next RunSaga rolls the saga back, as it would have had the failure been recorded before the crash.
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 ResolveStepHalt ¶ added in v0.8.0
func ResolveStepHalt(ctx context.Context, store Durable, runID, name string, result any, isError bool, opts ...ResolveOption) error
ResolveStepHalt clears a ResumeHalt on the Step named name, as ResolveHalt does for a tool call: it records the verified outcome as the step's result, so the resumed Step returns it (or, with isError, fails with it) instead of halting again. It takes the same options.
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 (SubRunID: the parent run ID, '>', the encoded tool-use ID). 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 answer is the arguments the final_answer tool accepted: what it received, after any tool middleware (a middleware that rewrites arguments rewrites the answer, as it would for any tool). The tool decodes them strictly as T (see Func: a missing required field, an unknown or case-variant name, a duplicate name, and so on are a tool error the model corrects) and journals them as its result, {"accepted": <arguments>}. RunTyped reads that JOURNALED result, not a value captured live, so it is resume-safe: on resume, or when the run already finished, the answer is recovered from the log without re-running anything. (A run journaled before final_answer recorded its arguments has the result {}; its answer is the model's arguments, decoded with encoding/json as the tool then accepted them.)
Only if the model never makes an accepted final_answer call (it answers in plain text instead) does RunTyped parse the text of the run's final turn (the message Run returns) as T, strictly too; a text that does not decode is ErrProtocol.
T must be a type whose schema is a JSON object (a struct, a pointer to one, or a map): the answer is a tool call's arguments, which providers take only as an object. Any other T is an ErrConfig before the run starts. 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). An adapter that does not support response formats (Anthropic) fails the run with ErrConfig before calling the model; use the provider-agnostic RunTyped there. Package function (Go methods can't add type parameters).
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 ResolveStepHalt (the halt's ToolUseID is the step name; its ToolName is empty) 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.
name must not start with a prefix the engine reserves for its own journal keys ("@", "run:", "tool:", "attempt:", "approval:", "signal:", and the rest; see IsReservedStepName): such a name is ErrConfig.
func SubRunID ¶ added in v0.8.0
SubRunID returns the run ID of the sub-agent run that the call toolUseID of run parent starts: parent, '>', then toolUseID encoded (see encodeID). It is RunScope inside that call.
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 ToolErrorText ¶ added in v0.8.0
ToolErrorText returns the text an agent journals, and sends to the model, for a call to the tool named tool that failed with err: the text chosen by the agent's WithToolErrorRedactor, if ctx is a tool call's context (as a ToolMiddleware receives it) from an agent with one set, and otherwise err's own text, in either case with every URL in it redacted. A tool middleware that records a failed call's error text (a trace span, a log line) uses it to record no more than the journal holds.
func ToolResultStep ¶ added in v0.8.0
ToolResultStep is the key of the result of the call toolUseID: the record the loop, a saga rollback, or ResolveHalt writes when the call's outcome is known.
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 WithModel ¶ added in v0.8.0
WithModel returns ctx under which the agent's model handler sends requests to m instead of the agent's model, so a middleware can send a call to another model through the rest of the chain (Hedge sends its backups this way). ok is false, and ctx is returned unchanged, when ctx is not the context of an agent model call.
func WithModelCallHook ¶ added in v0.8.0
WithModelCallHook returns ctx with h added to the hooks run around each model request made under it. Hooks run in the order they were added, so a middleware nearer the outside of the chain runs its Before first. ok is false, and ctx is returned unchanged, when ctx is not the context of an agent model call (a handler called directly, outside Agent), where nothing runs hooks: the middleware must then apply its effect itself.
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. Tool names must be unique: if two tools share a name, every run fails with ErrConfig.
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.
The first drive of a run records its input, and a run that has not finished resumes only with that input: another input is ErrConfig, as is resuming through RunSaga a run started through Run, or the reverse (see RunStart; RecordedStart reads the recorded input back).
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. A failure a human then records with ResolveHalt (isError) is a failed step: the next RunSaga rolls back.
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.
The id must not contain '>'. The session journals under "<id>>@session" and runs its turns under "<id>>@turn/<n>" and "<id>>@event/<encoded key>" (and their sub-agents under SubRunID(<turn run>, <call>)): everything up to the first '>' is the session id, so two sessions never share a run, and a root run ID may not contain '>', so no root run shares one with a session either.
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").
The system message is configuration, not journal: it is not recorded, and the model turns a drive makes are sent the message the agent holds at that drive. A run resumed after the prompt was changed sends its remaining turns the new prompt beside turns that answered the old one. Its recorded turns are not affected, and neither is any decision the loop makes on resume.
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.
fn is called once per drive of a run (each Run, Stream, or resume), not once per run, and its result is not journaled (see WithSystemPrompt): a run resumed later is sent what fn returns then. Context that the run's later turns must see unchanged belongs in the input, which is journaled (see RunStart), or in a tool result.
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 counts every model request the run sent, not only the responses it recorded: a failed attempt a middleware retried, a losing hedge target, and a model call that failed for good all count (see Result.Spend). It is durable: each turn's usage and discarded spend are journaled with it, a failed call's spend is journaled as its own record, and the count is rebuilt from the journal, so a resumed run, on any process, is held to what it has already used. A request still running when the run returns (a hedge loser whose model ignores cancellation) is not counted. Like WithMaxTurns it applies per run (per Session.Send turn).
The budget covers the run's agent tree: every run started from its tool calls (sub-agents, and theirs) counts against it, and each of those runs checks it, with its own budget if it has one, before each model call. A resumed tree first counts the journals of its sub-agents cut off mid-run, so it is held to everything it used before any of it calls the model again. Overshoot: a check sees every call that has returned, so the tree passes max only by calls in flight when it reached max. Each agent run has at most one model call in flight, so with k runs of the tree calling the model at that moment (k parallel sub-agents; 1 with none) the tree uses less than max plus k calls' usage.
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, TurnRestarted, 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, every approver id non-empty valid UTF-8, no two ids the same approver, and 1 <= Need <= len(Approvers). A policy that fails is a configuration error; the gate refuses it with ErrConfig rather than guessing.
Approver ids are compared as exact bytes everywhere else (eligibility, the signed decision bytes, the verifier lookup). A policy may still not list two ids that differ only by case or by Unicode normalization ("alice" and "Alice", an NFC and an NFD "café", a fullwidth and an ASCII spelling): a person reading the policy, or a key lookup that folds case, takes them for one approver, who could then fill two seats. Two ids are the same approver when their approverFoldKey is equal, and such a policy is refused as ambiguous.
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 arguments the tool accepted (after tool
// middleware; see CompensatedFunc); 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 Describer ¶ added in v0.8.0
type Describer interface {
Describe() ModelInfo
}
Describer is implemented by a Model that can say what it is. It is optional: the agent never requires it, and ModelInfoOf reports false for a Model that does not implement it.
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. A NaN or infinite Score (cosine similarity against a zero vector is 0/0) has no JSON encoding, so the retrieval helpers drop it (report 0).
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. A recorded Record is read back with
// DecodeStoredRecord, which refuses a row whose record names another step.
// If fn errors, nothing is recorded — the step re-runs on the next attempt. Once fn has
// returned a record, Do records it even if ctx was cancelled meanwhile: fn may have fired a
// side effect, and its outcome must not be lost (see durabletest).
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, each read back with
// DecodeStoredRecord under the name it is stored under.
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 Finish ¶
type Finish struct {
Reason FinishReason
Raw string // the provider's own finish reason, as it sent it ("end_turn", "STOP"); empty if it sent none
Usage Usage
Discarded Usage
}
Finish ends a model turn. Reason says why the turn ended, in the neutral vocabulary below; an adapter maps its provider's reasons onto it where they enter, and keeps the provider's own value in Raw. A first-party adapter always sets Reason: a provider reason it does not know is passed through unchanged (Reason is then Raw), and a turn the provider ended without naming a reason is FinishStop with an empty Raw. A Model that does not know the reason may leave Reason empty, which counts as a natural stop.
The reason decides whether the turn is the model's answer. Stream.Message (and so every agent run) returns ErrOutputTruncated for FinishLength and ErrOutputFiltered for FinishFiltered rather than a message, since what arrived is only part of what the model would have said, and a run that recorded it would end with that part as its final answer. Any other reason is ErrStreamProtocol: the core does not guess what a provider's own word means.
The reason never decides whether tools run: the calls the turn carries do. A turn with calls runs them whatever its reason says (OpenAI reports stop under a forced tool_choice), and a FinishToolUse turn with no call is ErrStreamProtocol, since the calls it was for were lost.
Usage is what the turn's own response cost. Discarded is usage billed for responses the turn threw away (failed attempts a middleware retried, losing hedge targets); a live adapter leaves it zero, and a Model that replays a recorded turn reports the discarded spend it recorded there.
type FinishReason ¶ added in v0.8.0
type FinishReason string
FinishReason is why a model turn ended, in a neutral vocabulary every adapter maps its provider's reasons onto (see Finish). The set is closed: the core accepts only the constants below and the empty reason.
const ( FinishStop FinishReason = "stop" // the model ended its answer (end of turn, a stop sequence) FinishToolUse FinishReason = "tool_use" // the model ended its turn to call tools FinishLength FinishReason = "length" // the output hit its token limit (or the context window) and was cut off FinishFiltered FinishReason = "filtered" // a safety or content filter, or a refusal, stopped the output )
The neutral finish reasons (see Finish).
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 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 finds it with Capability, so a wrapper that implements Unwrap keeps its inner store's Lister.
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. (Do that writes a record hands back the decoding EncodeRecord's round trip already made, when that is the stored bytes' decoding, rather than decode the same bytes twice.)
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): it retrieves the top-k documents for the run's user message and adds them, on every model call of the run (so the call that follows a tool result still has them), as a user message placed just before that user message: after the agent's system prompt and any earlier turns. The documents are data the operator does not control, so they are not given system authority, and each is written as one line of JSON, so no document can forge another entry (see formatDocs).
Inside an agent run the retrieval is a journaled step (read-only, so a crash before it is recorded simply retrieves again): the run retrieves once, every later call of the run (including after a crash and resume, or from a second driver) is given the documents it recorded, and the journal holds what the model was shown. Retrieved text is therefore durable content, stored like a tool result. Called outside a run, where there is no journal, it retrieves for the latest user message on every call.
A retrieval error aborts the model call (and nothing is recorded, so the next attempt retrieves again): have your Retriever return (nil, nil) instead of an error if you prefer to degrade to no context. A Retriever that returns more than k documents is cut to its first k. It panics if k is below 1.
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. Spend is replayed too: a turn reports the usage the original turn discarded (failed attempts, losing hedge targets) along with its own, and a model call that failed for good in the original fails again at the same point, reporting the same usage, so the replayed run's Result.Spend, journal and WithTokenBudget stops match the original's. A middleware in the replaying agent that retries such a failure moves on to the next recorded response instead. This is only possible because the durable substrate records a complete, replayable history in the first place.
type ModelCallHook ¶ added in v0.8.0
type ModelCallHook struct {
// Before runs before the request is sent, with the request's context. An error fails the
// call without sending it.
Before func(ctx context.Context) error
// After runs once a sent request has ended, successfully or not, with the usage it reported:
// for a failed request, the part reported before it failed.
After func(u Usage)
}
ModelCallHook runs around each request the agent's model handler sends to a model: every retried attempt and every hedged target, wherever the middleware that installed it sits in the chain. Middleware whose effect belongs to requests actually sent, rather than to calls through it, uses one: RateLimit waits for a token in Before, Cost counts spend in After.
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). When the turn's model call starts over (see TurnRestarted), the deltas before it are not part of the recorded turn.
type ModelHandler ¶
ModelHandler generates one assistant turn. Middleware wraps it.
type ModelInfo ¶ added in v0.8.0
ModelInfo identifies a Model. Provider names the API it calls ("anthropic", "openai", "gemini"), Model is the provider's model ID the requests name, and ResponseFormat reports whether the Model honours Request.ResponseFormat (a Model that does not fails a request that sets it, rather than drop the constraint).
func ModelInfoOf ¶ added in v0.8.0
ModelInfoOf describes m. It returns m's own Describe when m implements Describer, and otherwise follows an Unwrap() Model method, as a wrapping Model (middleware, a test double) offers, to the first Model in the chain that does. It reports false when no Model in the chain describes itself, when the chain ends in nil, or when it is longer than 64 links.
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 is token usage. On a StepModel record, the call's. On a StepToolResult or StepSagaFail
// record, the Usage of the runs the tool call started (sub-agents): their recorded responses.
Usage *Usage `json:"usage,omitempty"`
// DiscardedUsage is billed usage no recorded response carries. On a StepModel record, the
// turn's other model requests: failed attempts a middleware retried, losing hedge targets. On
// a StepValue record named "@spend/<n>", a model call that failed for good. On a
// StepToolResult or StepSagaFail record, the rest of the Spend of the runs the tool call
// started. Nil when there was none. WithTokenBudget counts it.
DiscardedUsage *Usage `json:"discarded_usage,omitempty"`
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"`
// ReadOnly marks a StepToolResult for a call whose tool was declared ReadOnly when the call
// ran. A saga rollback skips such a call (it changed nothing) and treats any other completed
// call as a write, whatever the tool is declared as by the time the rollback runs. A result
// that ResolveHalt injected is never ReadOnly: only a call that was not retry-safe halts.
ReadOnly bool `json:"read_only,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.
func DecodeStoredRecord ¶ added in v0.8.0
DecodeStoredRecord decodes the record a store holds for the step name of run runID (see DecodeRecord) and checks that the record carries that name. Every record is journaled with the name it is stored under (JournalEntry), and the engine finds records by the name inside them (IsComplete, the approval tally, a signal, an attempt marker), so a row whose record names another step (a row edited or copied in the backing store) would be read as that step: a row under "x" holding a record named run:complete would mark an unfinished run complete. Such a row is ErrStorage, naming the run and the key, like any other stored record that does not decode: the store's contents are wrong, whatever wrote them. Fields this version does not know still decode, so a journal a newer version wrote stays readable.
Every Durable implementation must read a record back through it, in Do and in History, or hand back a record known to be what it returns (MemStore.Do keeps the decoding it made while encoding the record it writes, when that is the stored bytes' decoding, and checks the name).
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 names the worker that claims leases, as the leases table and logs show it. Defaults to a host/pid/random string. The name identifies, it does not grant: each Lease call (and each run Recover drives) claims under a token of its own derived from it, so two drivers sharing a name still exclude each other, and a restarted worker waits out its predecessor's lease (the TTL) like any other process rather than taking it over.
func WithLeaseTTL ¶
func WithLeaseTTL(d time.Duration) RecoverOption
WithLeaseTTL sets how long an acquired lease is valid. Lease renews it while a run is driving, so a crash lets the lease expire after roughly this long and another process (RecoverLoop) takes over. Defaults to 30s. Set it well above the store's round-trip time and the longest pause you expect a process to take: renewal starts at half the TTL, and a drive that cannot renew within three quarters of it is cancelled with ErrLeaseLost (see Lease). It must be positive: Lease, Recover and RecoverLoop return an ErrConfig error otherwise.
func WithRecoverConcurrency ¶ added in v0.8.0
func WithRecoverConcurrency(n int) RecoverOption
WithRecoverConcurrency caps how many runs RecoverLoop drives at once. Defaults to 16. It must be at least 1. Only RecoverLoop reads it.
func WithRecoverErrors ¶ added in v0.8.0
func WithRecoverErrors(fn func(error)) RecoverOption
WithRecoverErrors sets the function RecoverLoop hands each genuine failure to: a store error while enumerating runs or acquiring a lease, or a drive that failed. Pauses and lost leases are not failures and are not reported. Without it, RecoverLoop drops failures (the next pass retries the run). It may be called from several goroutines, one call at a time. Only RecoverLoop reads it.
func WithRecoverInterval ¶ added in v0.8.0
func WithRecoverInterval(d time.Duration) RecoverOption
WithRecoverInterval sets how often RecoverLoop starts a recovery pass. Defaults to half the lease TTL, so a dead holder's run is taken over within about 1.5 TTLs of its last renewal. It must be positive. Only RecoverLoop reads it.
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; a marker whose AttemptedAt is zero or negative carries none (a negative value is not one the engine writes). Returns *HaltTooYoung when the halt has not aged enough, including a marker stamped in the future, 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). An adapter that does not support it (Anthropic) fails the request with ErrConfig rather than drop the constraint. 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 the usage of the model responses the run recorded, one per turn
// (input, output, and cache tokens). It is the whole run's, read from the journal: the same
// whether the run finished in one invocation or was resumed after a crash, and the same again
// when a finished run is re-entered.
Usage Usage
// Spend is every token the run's model requests used: Usage plus the usage of requests whose
// responses were discarded (failed attempts a middleware retried, losing hedge targets) and of
// model calls that failed. It is what the provider bills, and what WithTokenBudget counts.
// Like Usage it is the whole run's, from the journal (see Record.DiscardedUsage).
Spend Usage
// Turns is the number of live model turns this invocation executed. Turns replayed from
// the journal are not counted: unlike Usage and Spend, it describes this invocation.
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 (including a marker stamped zero or negative, see markerTime). 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 RetrievalOption ¶ added in v0.8.0
type RetrievalOption func(*retrievalToolConfig)
RetrievalOption configures a RetrievalTool.
func RetrievalDescription ¶ added in v0.8.0
func RetrievalDescription(description string) RetrievalOption
RetrievalDescription sets the description the model reads to decide when to call the tool, such as what the store holds.
func RetrievalName ¶ added in v0.8.0
func RetrievalName(name string) RetrievalOption
RetrievalName names the tool (the default is "retrieve"), for an agent that searches more than one store, each through its own RetrievalTool.
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.
Retrieve must be safe for concurrent use: parallel tool calls in one turn, concurrent runs on one Agent, and sub-agents sharing a Retriever call it at once.
type RunStart ¶ added in v0.8.0
RunStart is how a run was started, as its first drive records it: the input it answers (for a Session turn, the turn's message) and whether it runs as a saga (RunSaga, StreamSaga, RunSagaResult, or a sub-agent called inside a saga). A run's model turns and tool calls answer that input under that entry point's rules, so every later drive is held to it: resuming an unfinished run with another input, or through the other entry point (Run for a saga, RunSaga for a run), is ErrConfig. A finished run returns its recorded answer whatever it is passed, as before.
A run whose earlier drives predate this record gets it on its first drive under this version, with the input and entry point that drive is given.
func RecordedStart ¶ added in v0.8.0
RecordedStart returns how runID was started (see RunStart), and ok=false for a run whose journal holds no such record: one never driven, or one not driven since before the record existed. A session's turn run (IsSessionRun) records its message, but only the session can drive it (it seeds the turn with the transcript before that message), so Recover never hands one to its callback. A recovery callback uses it to re-drive a run with its own input and entry point:
start, ok, err := agent.RecordedStart(ctx, store, runID)
if err != nil {
return err
}
if !ok {
start = startFor(runID) // the deployment's own record, for a run not driven under this version
}
if start.Saga {
_, err = a.RunSaga(ctx, runID, start.Input)
} else {
_, err = a.Run(ctx, runID, start.Input)
}
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 in the session's journal, so a Session reloaded (after a restart) from the same store resumes the conversation.
Layering: a turn runs under its own run ID ("<id>>@turn/<n>" for Send, "<id>>@event/<key>" for SendOnce, the key encoded), whose durable journal handles crash resume WITHIN the turn; the session's journal, "<id>>@session", records which message started each Send turn and each completed turn's (input, answer) so the transcript can be rebuilt. A root run ID may not contain '>', so no run started with Run shares a journal with a session, and IsSessionRun tells a session's run IDs apart from every other. Recover skips them: a turn is seeded with the transcript before it, which only the session holds, and its answer is recorded only by the session, so an unfinished turn resumes when its message is sent again (the same Send, or the redelivered SendOnce). 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. Each turn also journals the transcript it started from (as "from/<turn run>"), so a turn resumed after a crash is seeded with exactly the turns its earlier model calls saw, even if other messages were answered in between.
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.
A Session is safe for concurrent use: callers sharing one handle behave as callers on separate handles do, and their turns run in parallel.
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>>@event/<key>" (the key encoded, so any key is allowed), so a different message arriving in between gets its own turn. Reusing a key with a different input is ErrConfig, whether the key's turn has finished or is still open: the turn's run records the message it answers (see RunStart).
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 // StepNotStarted records that the attempt whose marker it names never called its side // effect, written by the driver that claimed it (see attempt.go). That attempt then no // longer halts a resume, and the effect is re-attempted under a new marker. StepNotStarted StepKind = "not_started" )
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: the call's recorded arguments decoded as the forward call decoded them, strictly (see Func). A record written before tool arguments decoded strictly may hold arguments only encoding/json accepts; the forward call of that time decoded them with encoding/json, so compensation does too, and undoes the value that call acted on. Arguments neither decodes are ErrProtocol.
The arguments are the ones the tool accepted. When a tool middleware changes a compensable call's arguments in a saga, the arguments the tool receives are journaled before it runs (under a key of their own), and compensation undoes those; otherwise the model's arguments are the ones the tool received. A journal written before accepted arguments were journaled has no such record, so compensation there falls back to the model's arguments. A middleware that rewrites a retry-safe compensable call's arguments must rewrite them the same way every time: the first record is kept when the call runs again.
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 decoded from the args strictly, so the tool reads exactly what the model sent: a missing required field (one schema.For lists as required), null for a required field whose schema does not admit null, an unknown name or a case variant of a field's name, a duplicate name, data after the value, invalid UTF-8, or an escaped lone surrogate is ErrToolArgs, which goes back to the model as a tool error to correct. Empty args are the empty object. 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 ¶
func RetrievalTool(r Retriever, k int, opts ...RetrievalOption) Tool
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: a Retriever that returns more is cut to its first k. Read-only (retry-safe). The tool is named "retrieve" unless RetrievalName says otherwise; an agent's tools need distinct names, so give each its own when one agent searches several stores. It panics if k is below 1 or a name is empty.
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 (SubRunID(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 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, immediately before the tool is called. A call that does not start (cancelled after its attempt marker and before the call, and recorded as not started) emits neither ToolStarted nor ToolCompleted.
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 TurnRestarted ¶ added in v0.8.0
type TurnRestarted struct{ Seq int }
TurnRestarted fires when the model call for turn Seq starts over after an attempt that had already streamed ModelEvent deltas: a middleware such as Retry called the model again after that attempt failed, or delivered a different response in its place. The deltas received since TurnStarted{Seq} (or the previous TurnRestarted) belong to the discarded attempt and are not part of the recorded turn, so a consumer rendering the turn should clear them. Not emitted when the discarded attempt streamed nothing.
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).
// runID is the top-level run; name identifies the timer within it (for a timer inside a
// sub-agent it is qualified by the sub-run, so same-named timers of two sub-agents differ).
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
¶
- agent.go
- approval.go
- attempt.go
- awaitfor.go
- budget_tree.go
- channel.go
- compensate.go
- errors.go
- generate.go
- halt.go
- identity.go
- keys.go
- lease.go
- loop.go
- message.go
- model.go
- modelcall.go
- oncekey.go
- parallel.go
- pause.go
- record.go
- recovery.go
- redact.go
- render.go
- replay.go
- result.go
- retrieval.go
- runctx.go
- saga.go
- scripted_model.go
- session.go
- step.go
- store.go
- stream_agent.go
- subagent.go
- tool.go
- tool_middleware.go
- toolexec.go
- typed.go
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. |