Documentation
¶
Overview ¶
Package harness implements the agent's core loop as a state machine: stream a completion, recover from max-output-tokens / prompt-too-long, surface text/thinking/tool events, execute tool calls through a streaming executor (FIFO + exclusive scheduling), gate the Stop transition with hooks and a token budget, and repeat. It is exposed as a range-over-func iterator of Events and threads context cancellation throughout.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Compactor ¶
type Compactor interface {
// Pre conditions the history before a model request. lastInputTokens is the
// total token size the previous model response reported (0 if none yet), used
// as the initial gate count in preference to local estimation.
Pre(ctx context.Context, msgs []llm.Message, lastInputTokens int) []llm.Message
Reactive(ctx context.Context, msgs []llm.Message) ([]llm.Message, bool)
IsOverflow(err error) bool
}
Compactor is the harness's view of the compaction system (implemented in package compaction). Compaction is non-destructive; the harness sends only llm.MessagesForAPI(messages) to the model.
type ContextView ¶
ContextView is optionally implemented by a Compactor that builds the request message array itself, replacing llm.MessagesForAPI. Hosts with their own context manager (e.g. model-driven compression) implement it to project the history per request without mutating it.
View MUST be pure: the returned slice is used for this request only and is never written back to the loop's history. Implementations that depend on stable message identity (content hashing, per-message refs) rely on this — writing back would change message bytes and invalidate that identity.
type ContinueReason ¶
type ContinueReason string
ContinueReason names why the loop took another turn. These drive control flow; they are recorded on the loop for telemetry.
const ( ContinueNextTurn ContinueReason = "next_turn" ContinueReactiveCompactRetry ContinueReason = "reactive_compact_retry" ContinueMaxOutputTokensEscalate ContinueReason = "max_output_tokens_escalate" ContinueMaxOutputTokensRecovery ContinueReason = "max_output_tokens_recovery" ContinueStopHookBlocking ContinueReason = "stop_hook_blocking" ContinueTokenBudget ContinueReason = "token_budget_continuation" ContinueTaskNotification ContinueReason = "task_notification" )
type Event ¶
type Event struct {
Kind EventKind
Text string // KindText / KindThinking / KindProgress
ToolUse *llm.ContentBlock // KindToolUse
ToolResult *llm.ContentBlock // KindToolResult
Terminal *Terminal // KindResult
Usage *llm.Usage // KindUsage: cumulative usage after the latest model turn
}
Event is one item in the loop's output stream.
type EventKind ¶
type EventKind string
EventKind classifies a loop Event.
const ( KindText EventKind = "text" // assistant text delta KindThinking EventKind = "thinking" // reasoning delta KindToolUse EventKind = "tool_use" // a tool is about to run KindToolResult EventKind = "tool_result" // a tool finished KindProgress EventKind = "progress" // mid-tool progress KindUsage EventKind = "usage" // cumulative token usage so far (per model turn) KindResult EventKind = "result" // terminal event (loop finished) )
type HookRunner ¶
type HookRunner interface {
PreToolUse(ctx context.Context, name string, input []byte) (block bool, message string, updated []byte)
PostToolUse(ctx context.Context, name string, input, result []byte, isErr bool)
Stop(ctx context.Context, messages []llm.Message) (prevent bool, blockingErrors []string, message string)
}
HookRunner is the harness's view of the hook system (implemented in package hook). Stop returns two distinct outcomes: preventContinuation (hard terminate) and/or blockingErrors (inject + continue).
type QueryDeps ¶
type QueryDeps struct {
CallModel func(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error]
// CallModelSync is the non-streaming counterpart of CallModel, used when
// QueryInput.NonStreaming is set. It performs a single request and returns the
// fully assembled assistant message, the stop reason, and cumulative usage.
CallModelSync func(ctx context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error)
NewID func() string
}
QueryDeps are injectable side-effect boundaries for testing (FR-01.6).
type QueryInput ¶
type QueryInput struct {
Provider llm.Provider
System []string
DynamicBoundary int
Messages []llm.Message // conversation including the latest user turn
// SystemReminder, when non-empty, is prepended as the first message on each
// model call. It is not stored in conversation history — the host builds it
// fresh per Prompt call so it never accumulates across turns.
SystemReminder string
Tools *tool.Registry
// DeferredTools names tools whose schemas are withheld from the model's tool
// list (they stay in Tools and are invoked via ExecuteExtraTool). Empty = all
// tool schemas sent as usual.
DeferredTools []string
MaxTokens int
Temperature *float64
// MaxTurns bounds tool-turns (0 = off). MaxDuration bounds wall-clock time,
// checked at the turn boundary so a turn is never aborted mid-stream — unlike
// cancelling ctx, which cuts an in-flight stream/tool (0 = off).
MaxTurns int
MaxDuration time.Duration
MaxConcurrency int
// NonStreaming makes each model call a single non-streaming request (via
// QueryDeps.CallModelSync / Provider.Complete) instead of a token stream. The
// loop still emits the same KindText/KindThinking/KindToolUse/KindUsage events
// to the host — they are surfaced once, when the full response arrives, rather
// than incrementally. Zero value = streaming (unchanged default).
NonStreaming bool
WorkingDir string
AgentID string
// ToolOutputDir, when set, makes oversized tool output spill to a file there
// (full content kept) instead of being discarded; MaxToolOutputChars sets the
// per-tool output budget (head size; 0 = default 30000). See tool.Capture.
ToolOutputDir string
MaxToolOutputChars int
// Tasks, when non-nil, is the background-task manager shared with the tools.
// The loop drains its completion notifications into the conversation between
// tool turns so the model learns when background work finishes.
Tasks *tool.Manager
// Todos, when non-nil, enables todo system-reminders: when the model hasn't
// used TodoWrite for TodoReminderTurnsSinceWrite turns (and it's been at least
// TodoReminderTurnsBetweenReminder turns since the last reminder), the current
// list is re-surfaced as a <system-reminder> user message. Throttle turns are
// derived by scanning the API-visible history, so it survives compaction and
// resume. 0 thresholds default to 10/10 (mirrors claude-code).
Todos *tool.TodoStore
TodoReminderTurnsSinceWrite int
TodoReminderTurnsBetweenReminder int
// BashEnv holds extra "KEY=VALUE" entries injected into Bash subprocess
// environments (foreground + background). Empty = inherit unchanged.
BashEnv []string
// Permission policy.
PermissionMode permission.Mode
PermissionModeFunc func() permission.Mode
Allowed []string
Disallowed []string
CanUseTool permission.CanUseTool
// Hooks is optional lifecycle instrumentation. Nil disables it.
Hooks HookRunner
// Compactor is optional context-window management. Nil disables it.
Compactor Compactor
// Recorder, when non-nil, is notified as the conversation grows so a host can
// persist a transcript. It fires for genuinely new messages (births) in
// order, and for compaction boundaries; it is never called for compaction's
// ephemeral working-set edits (MicroCompact tool-result clearing), so the
// recorded transcript is the raw, full-fidelity history.
Recorder Recorder
// TokenBudget, when > 0, enables budget-driven continuation: after a turn
// with no tool calls, the agent is nudged to keep working until ~90% of the
// budget (output tokens), stopping early on diminishing returns.
TokenBudget int
// EscalateMaxTokens, when true, first retries a max_tokens truncation with a
// raised output cap (64k) before falling back to resume-recovery.
EscalateMaxTokens bool
// Settlement, when non-nil with a Prompt, runs a bounded wrap-up phase the
// moment a budget (MaxTurns/MaxDuration) is hit: it injects Prompt as a user
// turn and hides DisabledTools, so a budget-terminated run can checkpoint its
// findings instead of ending abruptly. The run still finishes with the budget's
// reason (ReasonMaxTurns/ReasonTimeout), never ReasonCompleted. Nil = off.
Settlement *Settlement
}
QueryInput is a fully-resolved request to run the loop.
type Recorder ¶
type Recorder interface {
RecordMessage(m llm.Message, usage llm.Usage)
RecordBoundary(meta llm.BoundaryMeta)
}
Recorder receives conversation events for durable transcript persistence (implemented in package transcript). RecordMessage fires once per new message in birth order; usage carries the token delta attributed to that turn (assistant messages only; zero otherwise). RecordBoundary fires when compaction inserts a boundary, for observability — boundaries are not part of the raw reconstructable history.
type Settlement ¶
type Settlement struct {
Prompt string // wrap-up instruction injected as a user turn (empty = settlement off)
MaxTurns int // turn budget for the wrap-up phase itself (0 = default 2)
DisabledTools []string // tool names hidden from the model during wrap-up (e.g. ["Bash"])
// PromptByReason optionally overrides Prompt based on WHY the run settled
// (keyed by the terminal reason, e.g. ReasonTimeout / ReasonMaxTurns). When the
// settling reason has a non-empty entry here it is used; otherwise Prompt is used.
// Lets a caller inject a different wrap-up depending on whether a run was cut by
// its time budget vs its step budget, without knowing which at build time.
PromptByReason map[TerminalReason]string
}
Settlement configures the wrap-up phase that runs when a budget is hit.
type Terminal ¶
type Terminal struct {
Reason TerminalReason
Err error
Usage llm.Usage
Turns int
Messages []llm.Message // full conversation snapshot (non-destructive: retains history)
Text string // the assistant's final text, for convenience
}
Terminal is the final state of a Query, delivered as the KindResult event.
type TerminalReason ¶
type TerminalReason string
TerminalReason explains why the loop stopped.
const ( ReasonCompleted TerminalReason = "completed" // model ended its turn ReasonBlockingLimit TerminalReason = "blocking_limit" // hard context limit hit pre-request ReasonImageError TerminalReason = "image_error" // unsupported media (multimodal) ReasonModelError TerminalReason = "model_error" // provider/API failure ReasonAbortedStreaming TerminalReason = "aborted_streaming" // canceled during streaming ReasonAbortedTools TerminalReason = "aborted_tools" // canceled during tool execution ReasonPromptTooLong TerminalReason = "prompt_too_long" // overflow recovery exhausted ReasonStopHookPrevented TerminalReason = "stop_hook_prevented" // a Stop hook blocked continuation ReasonHookStopped TerminalReason = "hook_stopped" // a tool/hook stopped continuation ReasonMaxTurns TerminalReason = "max_turns" // hit MaxTurns ReasonTimeout TerminalReason = "timeout" // hit a wall-clock run budget (host-imposed) )