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/plans/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 contextplan.Window, ...) error
- type AuditFunc
- type AuditKind
- type AuditRecord
- type Bounds
- type Conclude
- type ErrorFunc
- type ErrorPolicy
- type Loop
- type Options
- type Result
- type Steer
- type StopDecision
- type StopReason
- 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 ( // ErrNoCompleter is Validate's error when Completer is nil. ErrNoCompleter = errors.New("agentloop: completer is required") // ErrNoTools is Validate's error when Tools is nil. ErrNoTools = errors.New("agentloop: tools registry is required") // ErrMaxIterations is Validate's error when MaxIterations is // negative. Reserved for construction-time validation; Run itself // never returns it, since hitting MaxIterations at runtime is a // graceful StopMaxIterations stop, not an error. A zero value is // accepted and treated as uncapped (matches the legacy loop's // MaxSteps <= 0 == unbounded contract); see New's defaulting. ErrMaxIterations = errors.New("agentloop: MaxIterations must be non-negative") // 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, whatever the // cause: every tool lacking a schema, a Scope denying every tool, // or both together. ErrNoSchemas = errors.New("agentloop: registry offers no schema-bearing tool the scope allows") // 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 contextplan.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") // ErrSummarizerRequired is Options.Validate's error when Window is // set and Summarizer is nil. Test with errors.Is. ErrSummarizerRequired = errors.New("agentloop: Window requires Summarizer") // ErrEstimatorRequired is Options.Validate's error when Window is // set and Calibrated is nil. Guards the direct-Options path: a // caller set Window by hand without also setting Calibrated. See // also ErrNoTokenEstimator for the EnableCompaction path, which // checks the Completer's capability instead of the field. Test // with errors.Is. ErrEstimatorRequired = errors.New("agentloop: Window requires Calibrated") // ErrNoTokenEstimator is EnableCompaction's error when the // Completer lacks the provider.TokenEstimator capability, so // EnableCompaction has no estimator to fill Calibrated with. See // also ErrEstimatorRequired 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") // ErrTrimExcluded is Options.Validate's error when both Window and // Trim are set. Test with errors.Is. ErrTrimExcluded = errors.New("agentloop: Window and Trim are mutually exclusive") // ErrConcludeMargin is Validate's error when Conclude.Margin is // negative. Test with errors.Is. ErrConcludeMargin = errors.New("agentloop: ConcludeMargin must not be negative") // ErrMaxConcurrentTools is Options.Validate's error when // MaxConcurrentTools is negative. Zero means serial (today's // behavior); a positive value runs that many calls in parallel // through a worker pool. Test with errors.Is. ErrMaxConcurrentTools = errors.New("agentloop: MaxConcurrentTools must not be negative") // ErrMaxConsecutiveToolFailures is Validate's error when // MaxConsecutiveToolFailures is negative. Test with errors.Is. ErrMaxConsecutiveToolFailures = errors.New("agentloop: MaxConsecutiveToolFailures must not be negative") // ErrHeartbeatRequiresBus is Options.Validate's error when // HeartbeatInterval is positive and Bus is nil: a heartbeat with // nowhere to emit is a caller mistake, not a silent no-op. Test // with errors.Is. ErrHeartbeatRequiresBus = errors.New("agentloop: HeartbeatInterval requires a non-nil Bus") // ErrSessionIDRequired is Options.Validate's error when Usage is set // and SessionID is blank. Test with errors.Is. ErrSessionIDRequired = errors.New("agentloop: Usage requires a non-blank SessionID") // ErrMaxTotalTokens is Options.Validate's error when MaxTotalTokens // is negative. Test with errors.Is. ErrMaxTotalTokens = errors.New("agentloop: MaxTotalTokens must not be negative") // ErrMaxCallsPerTurn is Validate's error when MaxCallsPerTurn is // negative. Zero means unbounded. Test with errors.Is. ErrMaxCallsPerTurn = errors.New("agentloop: MaxCallsPerTurn must not be negative") // ErrConcludeDeadline is Options.Validate's error when // Conclude.Deadline is negative. Test with errors.Is. ErrConcludeDeadline = errors.New("agentloop: ConcludeDeadline must be non-negative") )
Sentinel errors for Options.Validate, 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.
Functions ¶
func Definitions ¶
Definitions builds []provider.ToolDefinition from reg, offering only a tool that publishes a schema through tools.SchemaOf and passes scope's check when scope is non-nil. Definitions fails closed: it returns ErrNoSchemas whenever reg holds at least one tool and the offered set ends up empty, whatever the cause. 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 contextplan.Window, alpha float64) error
EnableCompaction fills a Options' Window, Summarizer, and Calibrated fields from one Completer, in one call. The Completer must also implement provider.TokenEstimator; anthropic.Client does. window keeps the caller's configured trigger and target percentages. alpha is the calibration factor passed to contextplan.Calibrate. The Options must not already carry Trim; Window and Trim are mutually exclusive, and Validate rejects the pair.
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 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 means unbounded.
MaxCallsPerTurn int
// MaxTotalTokens caps the run's cumulative billed tokens. Zero
// 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 means unbounded.
MaxConsecutiveToolFailures int
}
Bounds groups the loop's numeric caps. Zero means uncapped or serial, per the member's own doc comment.
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.
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 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/plans/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.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
// OnToolCallError runs only on the ErrorPolicyReport path after a
// decodeAndRun or render failure, between the policy's
// report-to-model branch and the [tool-error] body construction. It
// lets a host synthesize a RoleTool message to append in place of
// the default body, or skip the call entirely by returning a
// non-nil error. Return (msg, nil) with a non-zero msg to append
// msg; return (nil, err) to skip the call and fail the run with
// err; return the zero Message and nil to fall through to the
// default [tool-error] body, preserving the pre-hook contract.
// Never fires under ErrorPolicyFail: that path hard-fails Run
// before consulting the hook.
OnToolCallError ErrorFunc
// Hooks fires PointPreTool and PointPostTool per tool call, and
// PointStop once at the end. Optional.
Hooks *hooks.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 *usage.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 *contextbudget.Limits
// Trim runs before each Completer call on the full message
// history. A nil Trim passes the history through unchanged. See
// docs/plans/agentloop.md for its contract with
// contextplan.Planner.Plan.
Trim func(ctx context.Context, msgs []provider.Message) ([]provider.Message, error)
// Surface, when non-nil, is consulted at the top of every
// iteration from the second one onward (after the steer
// injector drain, before the Completer call). The returned
// Surface replaces the iteration's advertised definitions,
// call-resolution registry, and scope; a nil return keeps the
// previous surface. A panic inside the hook fails the run.
// Optional; the default (nil) runs every iteration on the
// Options-level Tools and Scope unchanged.
Surface func() *Surface
// StreamingWriter, when non-nil, mirrors what the Completer
// writes through Request.StreamingWriter during a call. The
// loop buffers the same bytes; on a Steered stop the buffered
// bytes become Result.Final.Content, so a partial reply
// survives the cancel. A nil writer keeps Result.Final empty on
// Steered stop. Must be safe for concurrent use.
StreamingWriter io.Writer
// 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
// Window plans every iteration against a token budget. A nil Window
// disables planning; the loop then runs exactly as before. A non-nil
// Window requires Summarizer and Calibrated, and excludes Trim. When
// Budget is also set, Window's compaction runs before the Budget
// check, so Budget sees the compacted history, not the raw one.
Window *contextplan.Window
// Summarizer runs the LLM summary every compaction requires.
// Required when Window is set.
Summarizer *contextsummary.Summarizer
// Calibrated estimates tokens for planning and receives one Observe
// call after every Chat. Required when Window is set.
Calibrated *contextplan.Calibrated
// Conclude groups the graceful-conclude terms; see the Conclude
// type for Margin, Deadline, and Notice.
Conclude Conclude
// StartTime is the wall-clock anchor the SDK uses for the
// time-based Conclude.Deadline term. The work deadline the loop
// measures against is StartTime.Add(Conclude.Deadline) when
// Conclude.Deadline is positive; zero StartTime falls back to the
// time of New, so the threshold fires the deadline into the run
// from the moment of construction. Zero StartTime and zero
// Conclude.Deadline together disable the term entirely.
StartTime time.Time
// DedupWithinTurn detects a duplicate (tool, canonical-argument) call
// already served earlier in the same turn, and serves
// DuplicateCallNotice instead of running the tool again. False, the
// zero value, runs every call, unchanged from the base plan.
DedupWithinTurn bool
// 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
// WorkBudget, when non-nil, is a host-callable token-reservation
// surface the loop invokes around each Completer call. A non-nil
// WorkBudget requires both functions; Validate rejects a half-wired
// one with ErrIncompleteWorkBudget. See WorkBudget for details.
WorkBudget *WorkBudget
// ToolBudget, when non-nil, is a host-callable cumulative tool-call
// budget invoked once per turn before dispatch. The zero value
// (nil) disables it. See ToolBudget for details.
ToolBudget *ToolBudget
// ContinueOnStop is consulted when the loop is about to stop
// gracefully. A non-empty return appends those messages to the run
// history and continues the loop. A nil or empty return stops the run
// unchanged. A nil hook changes no behavior. Runs on the loop
// goroutine, like Surface; a panic fails the run closed. A
// continuation is an ordinary iteration and obeys every bound the
// loop owns. The loop adds no bound of its own for this hook: a
// caller that sets neither MaxIterations nor MaxTotalTokens and
// always returns messages gets an unbounded run. That is the
// caller's choice. See docs/plans/agentloop.md.
ContinueOnStop func(ctx context.Context, d StopDecision) []provider.Message
}
Options declares the blocks one New call wires into a Loop. Completer and Tools are required; the rest are optional.
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 contextbudget.Limits.Validate, a non-nil Window passes Window.Validate, requires Summarizer, requires 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.
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/plans/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 Options.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 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 Options.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.