Documentation
¶
Overview ¶
Package agentloop runs a model until it stops asking for tools. Run takes a provider.Completer, a tools.Registry, and a starting message list. It offers the registry's tools to the model, runs the tool calls the model requests, appends the results as provider.RoleTool messages, and repeats until the model returns no tool call or a bound trips. RunSteerable is Run with one addition: a caller-held Steer handle lets another goroutine request a graceful, in-flight stop of the current iteration, without a hard ctx cancellation. See docs/history/agentloop.md for the full contract.
Index ¶
- Constants
- Variables
- func Definitions(reg *tools.Registry, scope *tools.Scope) ([]provider.ToolDefinition, error)
- func EnableCompaction(o *Options, completer provider.Completer, window plan.Window, alpha float64) error
- func ToolCallFromContext(ctx context.Context) (provider.ToolCall, bool)
- func WithBatchOrder(ctx context.Context, order *BatchOrder) context.Context
- func WithToolCall(ctx context.Context, call provider.ToolCall) context.Context
- type AuditFunc
- type AuditKind
- type AuditRecord
- type BatchOrder
- type Bounds
- type Compaction
- type Conclude
- type ErrorFunc
- type ErrorPolicy
- type Extensions
- type Loop
- type Options
- type Result
- type Steer
- type StopDecision
- type StopReason
- type Summarizer
- type Surface
- type ToolBudget
- type WorkBudget
Constants ¶
const ( // EventIterationStart fires once at the start of every iteration. EventIterationStart events.Name = "agentloop.iteration.start" // EventCompletionHeartbeat fires every HeartbeatInterval while one // Completer call is in flight. EventCompletionHeartbeat events.Name = "agentloop.completion.heartbeat" // EventToolCallStart fires once at the start of every tool call, // before the PointPreTool hook fires. EventToolCallStart events.Name = "agentloop.tool_call.start" // EventToolCallHeartbeat fires every HeartbeatInterval while one // tool call is in flight. Never fires for a PointPreTool-vetoed // call, since a vetoed call never reaches the blocking work a // heartbeat reports progress on. EventToolCallHeartbeat events.Name = "agentloop.tool_call.heartbeat" // EventToolCallEnd fires once at the end of every tool call, // including a PointPreTool veto or hook-error return. EventToolCallEnd events.Name = "agentloop.tool_call.end" // EventIterationEnd fires once at the end of every iteration, // covering every exit path: a graceful stop or a hard-fail error. EventIterationEnd events.Name = "agentloop.iteration.end" // EventAssistant fires once per completed Completer turn whose // Message role is assistant, with the message content as Data. EventAssistant events.Name = "agentloop.assistant" // EventThinkingStart fires at the start of the thinking bracket // for one assistant turn that produced readable reasoning. EventThinkingStart events.Name = "agentloop.thinking.start" // EventThinkingDelta carries one assistant turn's readable reasoning // as Data, between EventThinkingStart and EventThinkingEnd. EventThinkingDelta events.Name = "agentloop.thinking.delta" // EventThinkingEnd closes the thinking bracket for one assistant // turn that produced readable reasoning. EventThinkingEnd events.Name = "agentloop.thinking.end" // EventCacheUsage fires after a Completer turn whose response // reported prompt-cache accounting; Data is the JSON-encoded // provider.CacheUsage. EventCacheUsage events.Name = "agentloop.cache_usage" // EventCalibrationDelta fires after every Calibrated.Observe call; // Data is the JSON-encoded calibrationPayload. EventCalibrationDelta events.Name = "agentloop.calibration_delta" // EventToolParallel fires once per turn dispatched with more than // one tool call, before the calls run; Data names the call count. EventToolParallel events.Name = "agentloop.tool_parallel" )
The Event Names agentloop emits on Options.Bus. The four lifecycle names fire once at their boundary whenever Bus is non-nil; the two heartbeat names tick at HeartbeatInterval only.
const CompactionNotice = "Earlier messages were compacted into a context summary. Some detail was dropped."
CompactionNotice is the user-role message content Run appends after a recovery compaction, so the model sees that compaction occurred.
const DefaultConcludeNotice = "You are close to the iteration limit. Provide your best final answer now."
DefaultConcludeNotice is Options.Conclude.Notice's fallback text.
const DuplicateCallNotice = "[duplicate-call] This exact tool call was already served earlier in this turn; skipped to avoid a repeated side effect."
DuplicateCallNotice replaces a tool result's content when DedupWithinTurn detects the same (tool, canonical-argument) call already served earlier in the same turn.
const RecoveryTargetTokens = 16384
RecoveryTargetTokens is the fixed compaction target of the prompt-too-long recovery path.
const ToolErrorPrefix = provider.ToolErrorPrefix
ToolErrorPrefix marks RoleTool message Content as an untrusted error report. runOneToolCall and decodeAndRun's validation-failure path both prefix error-report Content with it under ErrorPolicyReport, so the model-facing transcript distinguishes a reported failure from a normal tool result without a provider.Message schema change. The value lives on provider; this name re-exports it so existing callers keep one import.
Variables ¶
var ( // ErrUnrenderableResult is the render path's error when a tool // result's Out.Value cannot be marshaled to JSON after failing the // string and UTF-8-bytes cases. ErrUnrenderableResult = errors.New("agentloop: tool result cannot be rendered") // ErrCallsPerTurnExceeded is Run's error when one turn's response // requests more calls than a positive MaxCallsPerTurn allows. This // trip always fails the run, before any call in the turn runs, // regardless of OnToolError. ErrCallsPerTurnExceeded = errors.New("agentloop: turn requested more calls than MaxCallsPerTurn allows") // ErrNoSchemas is Definitions's error when the registry is // non-empty and the offered tool set ends up empty. Past // ErrNoSchema, which fails a schema-less tool directly, that means // the scope denied every tool. ErrNoSchemas = errors.New("agentloop: registry offers no schema-bearing tool the scope allows") // ErrNoSchema is Definitions's error when a scope-allowed // registered tool publishes no parameter schema. A scope-denied // tool never reaches this check. Wrapped with the tool's registry // name. Test with errors.Is. ErrNoSchema = errors.New("agentloop: registered tool publishes no parameter schema") // ErrOverBudget is Run's error when the message history, summed by // content bytes and message count, fails a non-nil Budget's Fits // check ahead of a Completer call. ErrOverBudget = errors.New("agentloop: message history exceeds Budget") // ErrTokenBudgetExceeded is Run's error when the run's cumulative // billed tokens exceed a positive MaxTotalTokens after a Completer // call returns. ErrTokenBudgetExceeded = errors.New("agentloop: cumulative tokens exceed MaxTotalTokens") // ErrInvalidSchema is New's error when a SchemaTool's // ParameterSchema() fails schema.Compile. Test with errors.Is. ErrInvalidSchema = errors.New("agentloop: tool parameter schema does not compile") // ErrArgumentValidation is decodeAndRun's error when // call.Arguments fails schema.Compiled.Validate against the called // tool's compiled parameter schema, before DecodeArguments runs. // Wraps the underlying schema error (schema.ErrValidation, // schema.ErrMalformedPayload, or schema.ErrAdmission). Routed // through OnToolError exactly like a DecodeArguments failure. Test // with errors.Is. ErrArgumentValidation = errors.New("agentloop: tool call arguments failed schema validation") // ErrToolNotOffered is decodeAndRun's error when a model-chosen call // names a tool with no entry in l.schemas, the schema set New // compiled once from the Scope-offered tools at construction time. // This happens when a caller registers a schema-bearing, // Scope-allowed tool on the shared *tools.Registry after New already // ran: Registry.Get and Scope.Allowed both read the live registry and // the live scope, so the call still reaches decodeAndRun, but // l.schemas, frozen at New, carries no entry for it. Routed through // OnToolError exactly like ErrArgumentValidation and // tools.ErrUnknownName. Test with errors.Is. ErrToolNotOffered = errors.New("agentloop: tool call names a tool not offered when New ran") // ErrPlanFailed is Run's error when the planning step cannot produce // an estimate or a plan: an estimator error or an invalid Window at // iteration time. Test with errors.Is. ErrPlanFailed = errors.New("agentloop: context planning failed") // ErrCompactionFailed is Run's error when a required compaction // cannot complete: the retention set alone exceeds the window // (wrapping plan.ErrRetentionOverflow), the summarizer call // failed (wrapping the contextsummary sentinel), or the compacted // history still exceeds the window. Test with errors.Is. ErrCompactionFailed = errors.New("agentloop: compaction failed") // ErrNoTokenEstimator is EnableCompaction's error when the // Completer lacks the provider.TokenEstimator capability, so // EnableCompaction has no estimator to fill Calibrated with. See // also ErrInvalidOptions for the direct-Options path, which // Validate raises once Window is set without Calibrated. Test // with errors.Is. ErrNoTokenEstimator = errors.New("agentloop: Completer does not implement provider.TokenEstimator") )
Sentinel errors for Definitions and Run; test with errors.Is.
var ErrIncompleteToolBudget = errors.New("agentloop: ToolBudget requires Reserve")
ErrIncompleteToolBudget is Options.Validate's error when ToolBudget is non-nil but Reserve is missing. Test with errors.Is.
var ErrIncompleteWorkBudget = errors.New("agentloop: WorkBudget requires both Reserve and Refund")
ErrIncompleteWorkBudget is Options.Validate's error when WorkBudget is non-nil but either Reserve or Refund is missing. Test with errors.Is.
var ErrInvalidOptions = errors.New("agentloop: invalid options")
ErrInvalidOptions is Options.Validate's, Bounds.Validate's, and Conclude.Validate's error for every configuration-shape check: an omitted required field, or a numeric field that fails its range rule. The wrapped message names the field and the rule it failed. Test with errors.Is; do not switch on message text.
Functions ¶
func Definitions ¶
Definitions builds []provider.ToolDefinition from reg, offering only a tool that passes scope's check when scope is non-nil. A scope-denied tool is skipped before its schema is ever read: denial is policy filtering, not a mistake, so a schema-less tool the scope excludes never fails Definitions. A scope-allowed tool that publishes no parameter schema through tools.SchemaOf fails Definitions with ErrNoSchema, wrapped with the tool's registry name. Definitions still fails closed with ErrNoSchemas when reg holds at least one tool and the offered set ends up empty; past ErrNoSchema that means the scope denied every tool. An empty reg returns an empty set and no error.
func EnableCompaction ¶ added in v0.4.0
func EnableCompaction(o *Options, completer provider.Completer, window plan.Window, alpha float64) error
EnableCompaction fills a Options' Compaction group from one Completer, in one call. The Completer must also implement provider.TokenEstimator; anthropic.Client does. A window with a positive MaxTokens keeps the caller's configured trigger and target percentages and lands in Compaction.Window. A window with MaxTokens at or below zero means derive: Compaction.Window stays nil and New derives 80/50 of the Completer's ContextWindow with one fifth held back as reserve. A negative value takes the derive path too, not an error: plan.Window.Validate rejects MaxTokens <= 0, so such a value is never a usable explicit window. alpha is the calibration factor passed to plan.Calibrate. The Options must not already carry Trim; Compaction.Window and Trim are mutually exclusive, and Validate rejects the pair. EnableCompaction and plan.NewSummarizer are the only sanctioned constructors for Compaction.Summarizer. A typed nil stored by hand is not nil as an interface; see the Summarizer interface for the warning.
func ToolCallFromContext ¶ added in v0.5.0
ToolCallFromContext extracts the provider.ToolCall attached by WithToolCall. Returns provider.ToolCall{}, false for a nil ctx or when absent.
func WithBatchOrder ¶ added in v0.5.0
func WithBatchOrder(ctx context.Context, order *BatchOrder) context.Context
WithBatchOrder attaches a batch's dispatch ledger to ctx.
func WithToolCall ¶ added in v0.5.0
WithToolCall attaches a provider.ToolCall to ctx. Exported so a Tool wrapped by an external caller's own tools.Tool implementation can recover the in-flight call's identity from the ctx this package passes into Tool.Run during tool dispatch (toolcall.go decodeAndRun).
Types ¶
type AuditFunc ¶
type AuditFunc func(ctx context.Context, rec AuditRecord) error
AuditFunc receives one AuditRecord per audited event, in the order Run produces them. A non-nil return is a hard failure: Run wraps it with the iteration count and returns it exactly like a Trim error, per the Result-shape rule.
type AuditKind ¶
type AuditKind string
AuditKind names which of Run's two audit-relevant events an AuditRecord describes.
type AuditRecord ¶
type AuditRecord struct {
// Iteration is the 1-based Completer-call count this record
// belongs to, matching Result.Iterations at the same point.
Iteration int
// Kind names which event this record describes.
Kind AuditKind
// Request is the exact provider.Request sent to Completer.Chat
// this iteration. Set only when Kind == AuditKindCompletion.
Request provider.Request
// Response is the provider.Response Completer.Chat returned this
// iteration. Set only when Kind == AuditKindCompletion.
Response provider.Response
// ToolCall is the model-requested call this record describes. Set
// only when Kind == AuditKindToolCall.
ToolCall provider.ToolCall
// ToolResult is the RoleTool message runOneToolCall appended to
// history for ToolCall, including any ToolErrorPrefix marker. Set
// only when Kind == AuditKindToolCall.
ToolResult provider.Message
// Err is the tool-run error runOneToolCall reported, or nil on a
// successful call. Set only when Kind == AuditKindToolCall.
Err error
// ThinkingContent is the response's readable reasoning text, copied out
// so a renderer can sign or audit it independently of Response.
// Empty on a completion whose assistant turn produced no reasoning
// and on every tool-call record.
ThinkingContent string
// CacheUsage is the response's prompt-cache accounting, copied out
// for the same reason. Reported is false on every completion whose
// Completer did not report cache usage and on every tool-call
// record.
CacheUsage provider.CacheUsage
}
AuditRecord is one audit-relevant event from a Run call, passed to Options.Audit. A caller builds and signs its own envelope.Message from the fields it needs; agentloop signs nothing itself.
type BatchOrder ¶ added in v0.5.0
type BatchOrder struct {
// contains filtered or unexported fields
}
BatchOrder is the per-turn dispatch ledger an agent loop publishes to the tools it runs. The dispatched set is the exact list of provider tool-call indices the loop hands to workers, fixed serially BEFORE any worker starts; Settle marks one index finished for ANY reason - the tool ran to completion, the call was rejected before the tools layer saw it, or the batch aborted before the call was claimed. The publishing loop guarantees every dispatched index settles exactly once, and that a call whose tool DID run settles only after the tool returned.
A tool that orders shared per-turn work by call index can therefore wait exactly: a dispatched, unsettled predecessor is either running or not yet scheduled - never a permanent hole - so no grace timer is needed to tell a scheduling gap from a skipped call. Exported so an external Tool wrapper can read the ledger this package attaches to ctx via WithBatchOrder.
func BatchOrderFromContext ¶ added in v0.5.0
func BatchOrderFromContext(ctx context.Context) (*BatchOrder, bool)
BatchOrderFromContext extracts the batch dispatch ledger from ctx.
func NewBatchOrder ¶ added in v0.5.0
func NewBatchOrder(dispatched []int) *BatchOrder
NewBatchOrder builds the ledger for one batch. dispatched is copied and sorted; indices absent from it are not part of the batch's contract.
func (*BatchOrder) Changed ¶ added in v0.5.0
func (b *BatchOrder) Changed() <-chan struct{}
Changed returns a channel that is closed on the next settlement after this call. Waiters re-fetch after each wake: the channel is swapped on every settle, so one settlement wakes every current waiter exactly once.
func (*BatchOrder) Dispatched ¶ added in v0.5.0
func (b *BatchOrder) Dispatched() []int
Dispatched returns the sorted dispatched indices as a copy.
func (*BatchOrder) Settle ¶ added in v0.5.0
func (b *BatchOrder) Settle(index int)
Settle marks index finished. Idempotent; every call past the first for the same index is a no-op, so defer-based settlement composes with explicit abort-path settlement.
func (*BatchOrder) Settled ¶ added in v0.5.0
func (b *BatchOrder) Settled(index int) bool
Settled reports whether index has settled.
func (*BatchOrder) UnsettledBefore ¶ added in v0.5.0
func (b *BatchOrder) UnsettledBefore(limit int) bool
UnsettledBefore reports whether any dispatched index below limit has not settled yet.
type Bounds ¶ added in v0.3.0
type Bounds struct {
// MaxIterations bounds the Completer-call count of one Run.
MaxIterations int
// MaxCallsPerTurn bounds one turn's model-requested tool calls.
// Zero inside a partial Bounds means unbounded.
MaxCallsPerTurn int
// MaxTotalTokens caps the run's cumulative billed tokens. Zero
// inside a partial Bounds means unbounded.
MaxTotalTokens int
// MaxConcurrentTools bounds one turn's parallel tool calls. Zero
// and one both mean serial.
MaxConcurrentTools int
// MaxConsecutiveToolFailures bounds consecutive all-failing turns.
// A turn counts as failing when every dispatched (non-duplicate)
// call in it carries a reported tool error under
// ErrorPolicyReport — any reported error, not only
// tools.ErrUnknownName. Zero inside a partial Bounds means
// unbounded.
MaxConsecutiveToolFailures int
}
Bounds groups the loop's numeric caps. Zero means uncapped or serial, per the member's own doc comment. The fully zero struct receives DefaultBounds at New; a partially set Bounds stays as given, so each zero member keeps its uncapped meaning.
func DefaultBounds ¶ added in v0.4.0
func DefaultBounds() Bounds
DefaultBounds returns a Bounds with every cap set to a sensible production default; see the per-member constants. The returned value passes Validate. Callers copy and adjust single members.
func (Bounds) Validate ¶ added in v0.3.0
Validate checks the caps in a fixed order and returns the first failure: MaxIterations, MaxTotalTokens, MaxCallsPerTurn, MaxConcurrentTools, then MaxConsecutiveToolFailures, each not negative. Every failure returns ErrInvalidOptions, wrapped with the failing field's name; test with errors.Is against ErrInvalidOptions.
type Compaction ¶ added in v0.5.0
type Compaction struct {
// Window plans every iteration against a token budget. Nil asks
// for derivation at New. Requires Summarizer and Calibrated, and
// excludes Options.Trim.
Window *plan.Window
// Summarizer runs the LLM summary every compaction requires.
// Required when Window is set. See the Summarizer interface for
// the sanctioned constructors and the typed-nil check Validate
// runs against them.
Summarizer Summarizer
// Calibrated estimates tokens for planning and receives one Observe
// call after every Chat. Required when Window is set.
Calibrated *plan.Calibrated
}
Compaction groups the context-window planning triple. The zero value disables planning; the loop then runs unplanned. Window nil with Summarizer and Calibrated set asks New to derive the window from the Completer's ContextAccountant capability.
type Conclude ¶ added in v0.3.0
type Conclude struct {
// Margin nudges the model once MaxIterations-k < Margin holds.
// Zero disables the step-count term.
Margin int
// Deadline, when positive, fires the nudge once
// StartTime.Add(Deadline) has passed. Zero disables the term.
Deadline time.Duration
// Notice is the RoleUser content Run appends once nudging starts.
// Empty Notice uses DefaultConcludeNotice.
Notice string
}
Conclude groups the graceful-conclude options: when the loop starts nudging the model toward a final answer, and what it says.
type ErrorFunc ¶
type ErrorFunc func(ctx context.Context, call provider.ToolCall, err error) (provider.Message, error)
ErrorFunc is the type of Options.OnToolCallError. The SDK invokes it on the ErrorPolicyReport path after a decodeAndRun or render failure. Returning a non-zero Message with nil error appends msg in place of the [tool-error] body. Returning an error fails the run with err wrapped under iteration and call.ID, with no RoleTool message appended. Returning the zero Message and nil preserves the default body. The function never runs under ErrorPolicyFail.
type ErrorPolicy ¶
type ErrorPolicy string
ErrorPolicy names what Run does with a tool-run error: report it to the model as a tool result, or end the run.
const ( ErrorPolicyReport ErrorPolicy = "" ErrorPolicyFail ErrorPolicy = "fail" )
ErrorPolicyReport is the zero value: a tool-run error, including a DecodeArguments failure, is sent back as the tool's RoleTool result content, and the run continues. ErrorPolicyFail turns the same error into Run's own hard-fail return.
type Extensions ¶ added in v0.5.0
type Extensions struct {
// OnToolCallError runs on the ErrorPolicyReport path after a
// decodeAndRun or render failure. See ErrorFunc for the contract.
OnToolCallError ErrorFunc
// Surface, when non-nil, replaces the iteration's advertised
// definitions, registry, and scope from iteration two onward.
Surface func() *Surface
// StreamingWriter, when non-nil, mirrors what the Completer
// writes; on a Steered stop the buffered bytes become
// Result.Final.Content. Must be safe for concurrent use.
StreamingWriter io.Writer
// Conclude groups the graceful-conclude terms; see the Conclude
// type for Margin, Deadline, and Notice.
Conclude Conclude
// StartTime is the wall-clock anchor for Conclude.Deadline. Zero
// falls back to the time of New.
StartTime time.Time
// DedupWithinTurn serves DuplicateCallNotice for a repeated
// (tool, canonical-argument) call within one turn.
DedupWithinTurn bool
// WorkBudget, when non-nil, is the token-reservation surface the
// loop invokes around each Completer call.
WorkBudget *WorkBudget
// ToolBudget, when non-nil, is the cumulative tool-call budget
// invoked once per turn before dispatch.
ToolBudget *ToolBudget
// ContinueOnStop is consulted on every graceful stop; a non-empty
// return continues the loop. See StopDecision.
ContinueOnStop func(ctx context.Context, d StopDecision) []provider.Message
}
Extensions groups the host-integration knobs one external loop mirror needs. Reached only through Options.Extensions; a nil pointer means every member at its zero value.
type Loop ¶
type Loop struct {
// contains filtered or unexported fields
}
Loop is a bound, ready-to-run tool-calling loop. Built only through New.
func New ¶
New validates opts, calls Definitions(opts.Tools, opts.Scope) once, and binds the result onto Loop. Run reuses that same []provider.ToolDefinition slice for Request.Tools on every iteration. New also compiles the parameter schema of every tool in defs, keyed by name, through schema.Compile; a compile failure fails New with ErrInvalidSchema. The compiled set is exactly the Scope-offered set defs already carries, so a malformed schema on a tool outside opts.Scope, or outside opts.Tools entirely, never fails this Loop's New call.
func (*Loop) Run ¶
Run calls Registry.RunScoped, never Registry.Run, so a model-chosen call always passes through l.scope. See docs/history/agentloop.md for the full termination and Result-shape contract. A wired Hooks registry fires PointStop exactly once, on every return path, with the returned Result as payload; a wired handler's veto or error never changes what Run already decided to return. See RunSteerable for a graceful, in-flight stop a caller can request mid-run.
func (*Loop) RunSteerable ¶
func (l *Loop) RunSteerable(ctx context.Context, msgs []provider.Message, steer *Steer) (Result, error)
RunSteerable is Run with one addition: a non-nil steer lets the caller request a soft-cancel of the current iteration's in-flight Completer.Chat call from another goroutine, through steer.Trigger. ctx cancellation still ends the run as a hard failure, unchanged from Run. Without an injector, a triggered steer ends the run gracefully at the next iteration boundary with Stop == StopSteered. With an injector installed, the run soft-continues and StopSteered never fires. See SetInjector. Final holds the zero value, except when Options.Extensions.StreamingWriter is set: then Final carries the bytes the Completer wrote before the steer, the same rule every other pre-response graceful stop already follows. History, Iterations, and Usage carry every already-completed iteration's state. Run(ctx, msgs) is equivalent to RunSteerable(ctx, msgs, nil).
type Options ¶
type Options struct {
// Completer runs each chat turn. Required.
Completer provider.Completer
// Tools is the registry Definitions builds the offered tool set
// from, and RunScoped resolves a model-chosen call against.
// Required.
Tools *tools.Registry
// Scope narrows which tools a model-chosen call may invoke. Run
// always calls Registry.RunScoped, never Registry.Run.
Scope *tools.Scope
// Model names the model Request.Model carries. An empty Model
// means the Completer's own default.
Model string
// Bounds groups the loop's numeric caps; see the Bounds type for
// the members and their zero values.
Bounds Bounds
// OnToolError governs what Run does with a tool-run error.
OnToolError ErrorPolicy
// Hooks fires PointPreTool and PointPostTool per tool call, and
// PointStop once at the end. Optional.
Hooks *events.Registry
// Tracer opens one span per iteration and one per tool call.
// Optional.
Tracer *trace.Tracer
// Usage records per-iteration provider.Usage under SessionID.
// Requires SessionID. Optional.
Usage *provider.Accumulator
// SessionID keys Usage's running total. Required when Usage is
// set.
SessionID string
// Bus receives Run's iteration, completion-heartbeat, and
// tool-call events. Required when HeartbeatInterval is positive;
// Run emits nothing through a nil Bus otherwise. Optional.
Bus *events.Bus
// Budget caps one Completer call's message history by byte count
// and message count. A nil Budget means uncapped. When Window is
// also set, Budget checks the history after window compaction runs,
// so a history Window would compact under Budget never fails here.
// When Window is nil, Budget checks history exactly as sent.
Budget *budget.Limits
// Trim runs before each Completer call on the full message
// history. A nil Trim passes the history through unchanged. See
// docs/history/agentloop.md for its contract with
// plan.Planner.Plan.
Trim func(ctx context.Context, msgs []provider.Message) ([]provider.Message, error)
// Audit receives one AuditRecord per completed Completer turn and
// per tool call whose result reaches history. A nil Audit means
// Run performs no audit call, at no added cost.
Audit AuditFunc
// Compaction groups the context-window planning triple. The zero
// value disables planning. See the Compaction type.
Compaction Compaction
// ObserveRequest runs after reserveWork and before every
// Completer.Chat call, including the prompt-too-long recovery retry's
// call. A non-nil error fails the iteration before the call runs.
// This is not Options.Audit: Audit records after the fact and cannot
// fail a call. A nil hook is a no-op.
ObserveRequest func(ctx context.Context, req provider.Request) error
// HeartbeatInterval emits a heartbeat Event on Bus every interval
// while one Completer call or one tool call is in flight. Zero
// disables heartbeats. A positive HeartbeatInterval requires a
// non-nil Bus.
HeartbeatInterval time.Duration
// Extensions holds the host-integration knobs. A nil Extensions
// means every knob at its zero value. Optional.
Extensions *Extensions
}
Options declares the blocks one New call wires into a Loop. Completer and Tools are required; the rest are optional. The host integration knobs live behind Extensions, the last field.
func (Options) Validate ¶
Validate checks Options in a fixed order and returns the first failure: Completer required, Tools required, Bounds.Validate (each cap non-negative), Usage requires a non-blank SessionID, a non-nil Budget passes budget.Limits.Validate, Compaction.Summarizer rejects a nil *plan.Summarizer typed-nil through a direct type assertion (unconditionally, since New's ContextAccountant-derived Window can adopt a typed-nil Summarizer even when Compaction.Window is left nil here; the module's reflection ban rules out a general nil-any check), a non-nil Compaction.Window passes Window.Validate, requires Compaction.Summarizer (rejecting an untyped nil), requires Compaction.Calibrated, and excludes Trim, Conclude.Validate (Margin not negative, then Deadline not negative), a positive HeartbeatInterval requires a non-nil Bus, and finally WorkBudget and ToolBudget each pass their own check. The three Extensions checks read the pointer nil-safely; a nil Extensions means every knob at its zero value. Every check in this fixed order returns ErrInvalidOptions, wrapped with the failing field's name and rule; test with errors.Is against ErrInvalidOptions, not message text.
type Result ¶
type Result struct {
// Final is the last message the model produced. Zero value when
// the stop happened before a new response arrived, or on
// StopHookVeto, StopMaxIterations, and StopSteered by design, and
// on every hard-fail return.
Final provider.Message
// History carries every message appended so far, including the
// caller's starting messages.
History []provider.Message
// Iterations counts the number of Completer calls that completed.
Iterations int
// Usage sums provider.Usage across every completed Completer call.
Usage provider.Usage
// Stop names why Run stopped gracefully. Zero value on a hard-fail
// error return.
Stop StopReason
}
Result holds a Run call's outcome. See docs/history/agentloop.md's Result-shape rule for how each field behaves on a graceful stop versus a hard-fail error return.
type Steer ¶
type Steer struct {
// contains filtered or unexported fields
}
Steer is a caller-held handle that requests a soft-cancel of one RunSteerable call's in-flight Completer.Chat call. Trigger is safe to call from another goroutine, any number of times, before, during, or after the RunSteerable call it is passed to. A Steer triggered before RunSteerable starts, or after it already returned, is a no-op: RunSteerable resets Steer's internal state at the start of its own call. One Steer value must not be passed to two concurrent RunSteerable calls: both calls would arm and disarm the same triggered flag and cancel func, and one caller's Trigger could stop the other caller's unrelated run.
func NewSteer ¶
func NewSteer() *Steer
NewSteer returns a ready Steer, unbound to any RunSteerable call until passed to one.
func (*Steer) HasActiveCall ¶
HasActiveCall reports whether a Completer.Chat call is currently in flight on this Steer (i.e. arm has bound a cancel func that disarm has not yet cleared). Continuous-bridge triggers fire on every poll tick; a trigger fired when no call is in flight is a no-op for the in-flight cancel but still sets the trigger flag for the next arm to observe. The next arm then immediately cancels that chat, then the bridge fires again, and the run never makes progress. Bridges that want to honor "fire only when there is a chat to cancel" can guard each Trigger call on this method, eliminating the no-op triggers that nevertheless poison the next chat's arm.
func (*Steer) SetInjector ¶
SetInjector installs f as the pull-based message source the loop consults at every iteration boundary and at every steered-stop decision. A non-nil return appends those messages to the run history and the run CONTINUES (a pending StopSteered is downgraded in that case). An empty return continues the run too: with an injector installed, every steered stop soft-continues and the return value never gates the stop. Stop the run by removing the injector or canceling ctx. Passing nil removes the injector.
SetInjector is meant to be called BEFORE RunSteerable starts. Once the run is in flight, SetInjector's effect on the next boundary is not formally defined by the doc comment; the implementation does not synchronize against reset(), but the loop never holds the mutex during an iteration body, so a SetInjector call between boundaries is observed at the next one. Calling SetInjector while the loop is inside an iteration body is therefore a race that may drop or apply the change on the next boundary depending on goroutine interleaving; this is the caller's responsibility to avoid, and the loop does not protect against it. The recommended pattern is: SetInjector once after NewSteer and before the first RunSteerable call, then never again.
The injector f runs on the loop goroutine; it must not block indefinitely or call back into the loop.
func (*Steer) Trigger ¶
func (s *Steer) Trigger()
Trigger requests the soft-cancel this Steer is bound to for its current RunSteerable call, if any. Trigger fired mid-tool-call-batch has no effect on the calls already dispatched in that batch; it takes effect at the start of the next iteration's Completer.Chat call instead. A Trigger call fired during a prompt-too-long recovery retry has no effect until the following iteration boundary, for the same reason. Calling Trigger more than once, or with no RunSteerable call in progress, has no additional effect.
type StopDecision ¶ added in v0.1.3
type StopDecision struct {
// Stop is the graceful reason the loop picked.
Stop StopReason
// Message is the assistant turn that ended the run.
Message provider.Message
// Iterations counts the Completer calls that completed.
Iterations int
// History carries every message appended so far.
History []provider.Message
}
StopDecision is the evidence the loop had when it decided to stop. Consulted only on a graceful stop; see Extensions.ContinueOnStop.
type StopReason ¶
type StopReason string
StopReason names why Run stopped gracefully. No StopToolError constant exists: a tool error under ErrorPolicyFail is a hard failure, not a graceful stop.
const ( // StopNoToolCalls is Run's stop reason when the model's response // carries no tool call and carries text content. StopNoToolCalls StopReason = "no_tool_calls" // StopEmptyResponse is Run's stop reason when the model returns no // tool call and no non-blank assistant text content. StopEmptyResponse StopReason = "empty_response" // StopMaxIterations is Run's stop reason when the iteration count // reaches Options.Bounds.MaxIterations. Not an error. StopMaxIterations StopReason = "max_iterations" // StopHookVeto is Run's stop reason when a PointPreTool handler // vetoes a tool call. The tool does not run. StopHookVeto StopReason = "hook_veto" // StopConcluded is Run's stop reason when the model returns no tool // call on an iteration Conclude.Margin nudged. Graceful, same // Result-shape rule as StopNoToolCalls. StopConcluded StopReason = "concluded" // StopSteered is Run's stop reason when a Steer.Trigger call requests // a soft-cancel of the in-flight Completer.Chat call. Graceful: nil // error, the same Result-shape rule as every other graceful stop that // happens before a new response arrives. StopSteered StopReason = "steered" // StopRepeatedToolFailures is Run's stop reason when consecutive // turns each fail all dispatched tool calls with unknown tool // errors, reaching MaxConsecutiveToolFailures. Graceful, same // Result-shape rule as StopMaxIterations. StopRepeatedToolFailures StopReason = "repeated_tool_failures" )
The declared StopReason values.
type Summarizer ¶ added in v0.5.0
type Summarizer interface {
Summarize(ctx context.Context, msgs []provider.Message) (plan.Summary, error)
}
Summarizer generates the summary one compaction requires. An implementation returns plan.ErrSummarySkipped to decline summary generation; compactHistory then reuses the prior summary or proceeds without one. Build the field's value only through EnableCompaction or plan.NewSummarizer. A typed nil (*plan.Summarizer)(nil) stored in the field is not nil as an interface; Validate asserts the field against that one sanctioned concrete type and returns ErrInvalidOptions when the assertion finds a nil pointer, the same as an untyped nil. This check runs unconditionally, whether or not Options.Compaction.Window is set, because New can derive a Window from the Completer's ContextAccountant capability even when Window is left nil. A custom Summarizer of some other pointer type holding a nil receiver is outside this check: only the sanctioned adapter's typed-nil shape is guarded.
type Surface ¶
type Surface struct {
// Advertised is the tool-definition list sent to the model this
// iteration. Replaces the previous iteration's set wholesale.
Advertised []provider.ToolDefinition
// Registry resolves model-chosen calls this iteration. Nil keeps
// the previous iteration's registry.
Registry *tools.Registry
// Scope narrows Registry lookups this iteration. Nil keeps the
// previous iteration's scope.
Scope *tools.Scope
}
Surface is one iteration's tool surface, produced by Extensions.Surface. Advertised is what the model is offered this iteration (compiled into the loop's schemas); Registry is where model-chosen calls resolve; Scope optionally narrows Registry. Advertised MAY name tools whose definitions are not backed by Registry entries and vice versa: the check kit treats the two sets as independent, mirroring hosts that advertise a union while gating execution through wrapper denial.
type ToolBudget ¶
type ToolBudget struct {
// Reserve runs once per turn before its tool calls dispatch, with
// the count about to run. A non-nil return fails the run.
Reserve func(ctx context.Context, calls int) error
}
ToolBudget is a host-callable cumulative tool-call budget the loop invokes once per turn, before that turn's tool calls dispatch. The SDK holds no budget policy of its own here, mirroring WorkBudget: Reserve runs host code (for example a shared call ceiling across concurrent subagent turns), and the loop only supplies the call point.
The loop calls Reserve exactly once per turn that has tool calls to run, with the number of calls about to dispatch (resp.ToolCalls before per-call filtering, dedup, or any MaxCallsPerTurn clamp), and BEFORE any of them run. A non-nil return hard-fails the run before dispatch, with none of the turn's tool calls executed.
There is no Refund: unlike a Completer call, a dispatched tool call is always consumed once Reserve admits the batch, so there is nothing to give back.
Reserve must be safe for concurrent use when callers share one Loop across concurrent Run calls.
type WorkBudget ¶
type WorkBudget struct {
// Reserve runs before each Completer call with the exact request
// about to be sent. A non-nil return fails the run.
Reserve func(ctx context.Context, req provider.Request) error
// Refund runs after the call outcome is known: zero Usage means
// the call never consumed its reservation; non-zero Usage carries
// the response's real billed usage.
Refund func(ctx context.Context, req provider.Request, used provider.Usage)
}
WorkBudget is a host-callable token-reservation surface the loop invokes around each Completer call. The SDK holds no budget policy of its own here: Reserve and Refund run host code (for example a shared token ceiling across concurrent subagent turns), and the loop only supplies the call points.
The loop calls Reserve once per iteration, with the exact provider.Request it is about to send, BEFORE l.completer.Chat runs. A non-nil Reserve return hard-fails the run before the call, wrapped with the iteration count.
The loop calls Refund after the call's outcome is known, with the same request: once with the zero provider.Usage when the call failed (the reservation was never consumed), or once with the response's real Usage when the call succeeded and reported a non-zero Usage. A successful call that reports the zero Usage gets NO Refund: the host keeps the reservation consumed, matching the legacy loop's consume-on-completion rule.
Both functions must be safe for concurrent use when callers share one Loop across concurrent Run calls.