Documentation
¶
Overview ¶
Package agent provides the core AI agent logic: task planning, tool invocation, and conversation.
Index ¶
- Constants
- Variables
- func Compact(ctx context.Context, opts RunLoopOpts) (CompactResult, RunStats, error)
- func CtxMarkSubagent(ctx context.Context) context.Context
- func CtxWithDelegatedUsage(ctx context.Context, u *DelegatedUsage) context.Context
- func CtxWithIteration(ctx context.Context, iter int) context.Context
- func CtxWithMemoryStore(ctx context.Context, s MemoryStore) context.Context
- func CtxWithNoteStore(ctx context.Context, s NoteStore) context.Context
- func CtxWithRunID(ctx context.Context, runID string) context.Context
- func CtxWithToolCall(ctx context.Context, toolCallID string) context.Context
- func CtxWithoutMemoryStore(ctx context.Context) context.Context
- func DeclaredAccess(tool llm.Tool, args map[string]any) llm.Access
- func EstimateMessageTokens(m llm.Message) int
- func EstimateTokens(msgs []llm.Message) int
- func ExecuteTool(ctx context.Context, t llm.Tool, tc llm.ToolCall) string
- func ExtractInvariants(systemPromptText string) string
- func IterationFromContext(ctx context.Context) int
- func RenderCompactionBlock(summary string) string
- func RenderMemoryIndex(index MemoryIndex) string
- func RenderSessionState(invariants string, notes []Note, todos []Todo) string
- func ResolveToolAction(policy ToolPolicy, tool llm.Tool, args map[string]any, interactive bool) llm.ToolAction
- func RunIDFromCtx(ctx context.Context) string
- func SubagentFromCtx(ctx context.Context) bool
- func ToolCallFromCtx(ctx context.Context) string
- func TrimHistory(msgs []llm.Message, systemTokens, contextWindow, reserveTokens int) []llm.Message
- func ValidateInstructionLayers(spaceInstructions, agentInstructions string) error
- func ValidateNotes(texts []string) error
- func ValidateTodos(todos []Todo) error
- type ApprovalDecision
- type ApprovalHandler
- type CompactResult
- type CompactionHistory
- type ContextCompactor
- type ContextSources
- type DelegatedStats
- type DelegatedUsage
- type Event
- type EventKind
- type HistoryProjection
- type HookDecision
- type HookEvent
- type HookInput
- type HookOutput
- type HookRunner
- type MemoryBody
- type MemoryIndex
- type MemoryIndexEntry
- type MemorySourceInfo
- type MemoryStore
- type MemoryUpsert
- type MessageHistory
- type MessageQueue
- type NoopSandbox
- func (NoopSandbox) AllowUnsandboxed() bool
- func (NoopSandbox) Backend() string
- func (NoopSandbox) ChildEnv() []string
- func (NoopSandbox) Enabled() bool
- func (NoopSandbox) HostAllowed(_ string) (bool, string)
- func (NoopSandbox) Mode() string
- func (NoopSandbox) ProxyAddress() string
- func (NoopSandbox) ScrubEnv(env []string) []string
- func (NoopSandbox) ShouldSandboxCommand(_ string) bool
- func (NoopSandbox) WrapBashCommand(_ context.Context, _, _ string) (string, []string, error)
- type Note
- type NoteStore
- type NotesHistory
- type PendingInput
- type PromptLayer
- type RunLoopOpts
- type RunStats
- type SandboxInfo
- type SandboxView
- type SessionGrants
- type StateCheckpointer
- type Todo
- type ToolBoundaryHistory
- type ToolCallStart
- type ToolOutcome
- type ToolPolicy
Constants ¶
const ( // ToolErrorInvalidArgs is arguments the model sent that would not parse. ToolErrorInvalidArgs = "invalid_args" // ToolErrorFailed is the tool itself returning an error. ToolErrorFailed = "tool_error" // ToolErrorPanic is a tool panicking, which is a defect in BuildMax. ToolErrorPanic = "panic" )
How a tool call failed. A denial is not among them: it has its own event and its own reason, and a call nobody allowed to run did not fail.
const ( DenyReasonPolicy = "policy" // blocked by ToolPolicy or tool's own DefaultAction DenyReasonUser = "user" // user denied the approval prompt DenyReasonLoopGuard = "loop_guard" // repeated identical call blocked by loop guard DenyReasonHook = "hook" // blocked by a PreToolUse hook DenyReasonUnknown = "unknown_tool" )
Deny reason labels for EventToolDenied.
const ( NotificationApprovalRequired = "approval_required" NotificationPermissionDenied = "permission_denied" )
NotificationKind enumerates the reasons a HookNotification event fires.
const ( ToolNameMemoryRead = "MemoryRead" ToolNameMemoryWrite = "MemoryWrite" )
Tool names the memory contract refers to. They are here rather than imported from internal/tool because core must not depend on it, and the preamble has to name the tool it tells the model to call.
const ( TodoPending = "pending" TodoInProgress = "in_progress" TodoCompleted = "completed" )
Todo statuses. These are the values a todo may carry; anything else is rejected.
const ( // MaxNotes is the most notes a session may hold. MaxNotes = 15 // MaxNoteChars bounds one note. Notes are one-liners; prose rots invisibly. MaxNoteChars = 200 // MaxTodos is the most todos a session may hold. MaxTodos = 30 // MaxInvariantChars bounds the restated invariants. They are repeated after the messages // on every call, so the section has to stay a reminder rather than a second copy of the // whole prompt. MaxInvariantChars = 1024 )
Bounds on durable state. These are not tuning knobs: this state is rendered into every request and, unlike history, has no trimming path, so an unbounded list here is worse than a lost message.
const ( ToolStatusCompleted = "completed" ToolStatusFailed = "failed" ToolStatusDenied = "denied" )
Tool outcome statuses.
These are the vocabulary a durable history records for a finished call. ToolStatusDenied is distinct from ToolStatusFailed because they mean different things to whoever reads the transcript later: denied means BuildMax refused, failed means the tool tried and could not.
const DefaultMaxQueuedMessages = 10
DefaultMaxQueuedMessages caps how many messages one queue holds. The cap exists so a user holding down enter during a long run cannot commit the runtime to an unbounded backlog of turns it will still be working through minutes later.
const MaxMemoryIndexChars = anchorBlockBudgetChars
MaxMemoryIndexChars bounds the rendered index.
It is the ceiling RenderSessionState already applies to invariants, notes, and todos combined, and for the same reason: both blocks sit after the message list, so both are paid for in fresh input tokens on every iteration of every session. The store's per-memory limits normally keep the index well under this; the renderer enforces it anyway, so a hand-edited store cannot exceed it.
const MaxUserAuthoredSystemPromptChars = 8192
MaxUserAuthoredSystemPromptChars bounds the Space and Agent instruction layers together. They are sent in full on every model call and have no trimming path.
Variables ¶
var ErrCompactionNotPersisted = errors.New("persist compaction")
ErrCompactionNotPersisted wraps a durable history that refused to record a compaction. It is separated from every other compaction failure because only this one is fatal: the summary exists and the boundary does not, so the run cannot continue against a history that disagrees with what was summarized.
var ErrMaxIterations = errors.New("max iterations exceeded")
ErrMaxIterations ends a run that reached its MaxIter bound.
It is a sentinel rather than a message because it is not a failure of the same kind as a provider outage: the run spent the budget it was given and stopped, and everything it did up to that point stands. A caller that cannot tell the two apart reports an exhausted budget as an incapable agent, which is the distinction docs/design/evaluation-system.md section 7.4 exists to keep.
var ErrQueueFull = errors.New("message queue full")
ErrQueueFull is returned by Enqueue when the queue is at its cap.
Functions ¶
func Compact ¶
func Compact(ctx context.Context, opts RunLoopOpts) (CompactResult, RunStats, error)
Compact compacts a history on demand, outside a run.
It is the same pass RunLoop makes when the context window fills, minus the fill test: a user who asks for compaction is not asking whether it is due. Only the fields compaction itself uses are read from opts — History, Compactor, Checkpointer, Hooks, LLMClient, Pricing, EventSink, and the attribution fields hooks are given.
The returned RunStats hold what the summarizing call spent, so a caller that keeps session totals can fold them in; they are reported even when the pass compacted nothing, because a call that produced an unusable summary was still paid for.
func CtxMarkSubagent ¶
CtxMarkSubagent marks ctx as executing inside a subagent run. A subagent's session is discarded when it returns, so tools that detach work owned by a session refuse under this mark rather than hand the work to an owner nobody can see.
func CtxWithDelegatedUsage ¶
func CtxWithDelegatedUsage(ctx context.Context, u *DelegatedUsage) context.Context
CtxWithDelegatedUsage returns a context carrying u as the accumulator that delegated runs report to. RunLoop installs its own, so a subagent's own delegations accrue to the subagent and reach the parent only through the subagent's totals.
func CtxWithIteration ¶
CtxWithIteration returns a context carrying the loop iteration a tool is being called from.
func CtxWithMemoryStore ¶
func CtxWithMemoryStore(ctx context.Context, s MemoryStore) context.Context
CtxWithMemoryStore carries the store for one run's tool calls. It is reached through the context rather than held on a tool, because the tool registry is cached per model and shared across sessions, so a tool holding a store would carry one session's Project into another.
func CtxWithNoteStore ¶
CtxWithNoteStore returns a context carrying the durable state store for the current run.
func CtxWithRunID ¶
CtxWithRunID marks ctx with the identity of the run (trace run) it belongs to.
func CtxWithToolCall ¶
CtxWithToolCall marks ctx as executing the given tool call.
func CtxWithoutMemoryStore ¶
CtxWithoutMemoryStore removes the store.
It is what a delegate runs under. A subagent's tool list already excludes both memory tools, but a capability that is only unreachable by convention is one an added tool or a user-defined agent definition can reach by accident.
func DeclaredAccess ¶
DeclaredAccess returns what a tool says this call does. A tool that declares nothing is llm.AccessWrite.
func EstimateMessageTokens ¶
EstimateMessageTokens returns a character-based token estimate for one message. Uses the standard 4-chars-per-token heuristic plus 4 tokens of overhead per message for JSON framing.
func EstimateTokens ¶
EstimateTokens returns the total estimated token count for a slice of messages.
func ExecuteTool ¶
ExecuteTool parses tc.Arguments and calls tool.Execute, returning the result or an error string. Useful for testing and single-call invocations that bypass the policy layer.
func ExtractInvariants ¶
ExtractInvariants returns the body of the "## Invariants" section of an additional system prompt, or "" when it has none. The section is restated after the messages on every call, because an instruction present verbatim in the system prompt still loses ground as the context fills with tool output: storage keeps it, proximity keeps it followed.
func IterationFromContext ¶
IterationFromContext returns the loop iteration, or 0 when it is not known.
func RenderCompactionBlock ¶
RenderCompactionBlock formats a compaction summary for injection into the system prompt. It is the single definition of that block so every caller produces the same shape and no two callers can append competing copies.
func RenderMemoryIndex ¶
func RenderMemoryIndex(index MemoryIndex) string
RenderMemoryIndex renders the block placed after the message list and before the session-state anchor.
The ordering is deliberate: memory is older, shared context, and session state is specific to the task in hand, so session state stays closest to generation. An empty store renders nothing.
func RenderSessionState ¶
RenderSessionState renders durable state as a block to be placed after the message list. Returns "" when there is nothing to say — a session with no invariants that never writes a note pays nothing.
invariants is the restated hard-constraint section of the run's additional system prompt; pass "" when there is none.
The block carries no ages. Entries were once stamped with the iteration they were written at — an absolute [i12] on a note, a relative ", 38 iterations" on the in-progress todo — and neither was ever shown to change what the model did. The block is re-rendered into every request, so an unproven line is a permanent cost; WrittenIteration is still recorded for the drift detection in docs/design/context-durability.md, which will read it rather than print it.
func ResolveToolAction ¶
func ResolveToolAction(policy ToolPolicy, tool llm.Tool, args map[string]any, interactive bool) llm.ToolAction
ResolveToolAction reports the action one tool call resolves to, without executing or prompting. An Ask returned here is a question that would be asked, not an outcome: with no handler the loop turns it into a denial.
func RunIDFromCtx ¶
RunIDFromCtx returns the current run's ID, or "".
func SubagentFromCtx ¶
SubagentFromCtx reports whether ctx runs inside a subagent.
func ToolCallFromCtx ¶
ToolCallFromCtx returns the executing tool call's ID, or "".
func TrimHistory ¶
TrimHistory returns a suffix of messages that fits within the token budget:
budget = contextWindow - reserveTokens - systemTokens
Tool-call groups are kept or dropped as a unit: an assistant message with ToolCalls and its paired tool-role messages are never split, because most LLM APIs reject sequences where one side of a tool call is missing.
When reserveTokens is 0, defaultReserveTokens (4096) is used. When the budget is too small to keep any messages, the most recent message is kept.
func ValidateInstructionLayers ¶
ValidateInstructionLayers applies the shared permanent-prompt budget.
func ValidateNotes ¶
ValidateNotes checks a proposed note list against the bounds and returns an error written for the LLM: it names the limit and what to do, because a tool failure the model cannot act on is a dead end.
func ValidateTodos ¶
ValidateTodos checks a proposed todo list against the bounds.
Types ¶
type ApprovalDecision ¶
type ApprovalDecision uint8
ApprovalDecision is what a user chose at an approval prompt.
const ( // ApprovalDeny blocks the call. ApprovalDeny ApprovalDecision = iota // ApprovalAllowOnce runs this call and asks again next time. ApprovalAllowOnce // ApprovalAllowSession runs this call and records a grant so the same // scope stops asking for the rest of the session. ApprovalAllowSession )
type ApprovalHandler ¶
type ApprovalHandler interface {
RequestApproval(ctx context.Context, name string, args map[string]any) ApprovalDecision
}
ApprovalHandler is invoked when the resolved action is ToolActionAsk. If nil, Ask collapses to Deny.
ctx is the run's context. A handler blocks a goroutine until a person answers, and a cancelled run may never get one -- so it must return on ctx.Done() rather than waiting for a prompt nobody will resolve.
type CompactResult ¶
type CompactResult struct {
// Summarized is how many model-visible messages the summary replaced.
Summarized int
// Kept is how many messages remain verbatim after the boundary.
Kept int
// Summary is what the summarizer produced, after clamping to its budget.
Summary string
// Reason explains a pass that compacted nothing. Empty when it did.
Reason string
}
CompactResult reports what one compaction pass did.
Summarized == 0 means nothing was replaced and Reason says why — an empty history, a hook that blocked, or nothing old enough to summarize. That is not an error: a compaction that finds no work to do leaves a usable session.
func (CompactResult) Compacted ¶
func (r CompactResult) Compacted() bool
Compacted reports whether the pass replaced anything.
type CompactionHistory ¶
type CompactionHistory interface {
MessageHistory
// PriorSummary returns the summary stored by the most recent compaction, or "" when the
// history has never been compacted.
PriorSummary() string
// AddCompaction advances the compaction boundary by summarizedCount messages and stores summary.
//
// It returns an error because a durable history commits here. Compaction
// changes what the model sees, so a boundary that failed to reach storage
// would leave the next turn reading a different conversation than this one
// ended with.
AddCompaction(summary string, summarizedCount int) error
}
CompactionHistory is an optional extension of MessageHistory implemented by persistent histories that can record a compaction boundary across turns. When RunLoop compacts the context, it calls AddCompaction so the next turn starts from the compacted view without re-summarizing.
The history owns the summary, and RunLoop reads it back through PriorSummary rather than expecting the caller to have folded it into SystemPrompt. That keeps one owner for the block: two owners is how the same summary ends up in the prompt twice.
type ContextCompactor ¶
type ContextCompactor interface {
// Usage is what the summarization itself cost. It is returned rather than
// left to the implementation to report because compaction is a model call
// the run caused: a total that omits it understates exactly the long
// sessions where it matters most. A compactor that does not call a model
// returns the zero Usage.
Compact(ctx context.Context, msgs []llm.Message) (summary string, usage llm.Usage, err error)
}
ContextCompactor summarizes a slice of messages into a short text that can replace them. The returned summary is injected into the system prompt so the LLM retains prior context.
type ContextSources ¶
type ContextSources struct {
// ProjectID names the scope shared memory belongs to, empty for a run that
// has none.
ProjectID string `json:"project_id,omitempty"`
// Workspace is the root this run executed against, which for a Project with
// several worktrees is not derivable from ProjectID.
Workspace string `json:"workspace,omitempty"`
// Instructions are the system-prompt layers, in order. Written even when
// there is nothing beyond the runtime prompt: an absent list would read as
// "nobody looked" rather than "there was nothing else".
Instructions []PromptLayer `json:"instructions,omitempty"`
// Memory is every fallible recall source this run loaded.
Memory []MemorySourceInfo `json:"memory,omitempty"`
// HistoryProjection describes what stood in for messages the run no longer
// holds in full.
HistoryProjection HistoryProjection `json:"history_projection"`
}
ContextSources is everything a run was given before its first model call, and where each part came from.
It exists because "memory" had been used for four different things -- an instruction layer, a compaction summary, session notes, a shared document -- and a diagnostic that called them all by one name could not answer which of them put a line in front of the model. Each source keeps its own kind here.
No raw text. The session bundle and the Project bundle already hold the content, trace redaction is fail-open, and a diagnostic that copied instructions and memory into a third file would widen the blast radius of every one of them to answer a question sizes and revisions already answer.
type DelegatedStats ¶
type DelegatedStats struct {
// Runs counts delegated runs, not the calls that started them.
Runs int
PromptTokens int
CompletionTokens int
CacheReadTokens int
CacheWriteTokens int
ToolCalls int
Cost *llm.Cost
CostIncomplete bool
}
DelegatedStats is what runs delegated from one run spent.
The token and cost fields break the RunStats fields beside them down rather than adding to them, the same way the cache counts break the prompt count down: a reader that sums both counts a delegation twice.
ToolCalls is the exception and is additional. A delegation is one tool call of the parent, already counted there; the calls the child made are its own and are counted nowhere else.
type DelegatedUsage ¶
type DelegatedUsage struct {
// contains filtered or unexported fields
}
DelegatedUsage collects what delegated runs spent, so the run that started them reports the bill it is actually paying.
It travels on the context because a delegation reaches the loop as an ordinary tool call, and llm.Tool.Execute returns a string. Widening that interface so one tool could report spend would put accounting into every tool in the registry. The lock is not decoration: tool calls within one iteration can execute in parallel, so two subagents can report at once.
func DelegatedUsageFromCtx ¶
func DelegatedUsageFromCtx(ctx context.Context) *DelegatedUsage
DelegatedUsageFromCtx returns the accumulator for the run that owns ctx, or nil when nothing is collecting. Nil is a normal state — a subagent runner invoked outside a run has nobody to report to — and every method tolerates it.
func (*DelegatedUsage) Drain ¶
func (d *DelegatedUsage) Drain() DelegatedStats
Drain returns what has accumulated since the last call and resets. The caller folds it into a running total, so returning the delta rather than the whole keeps repeated folding from double counting.
func (*DelegatedUsage) Report ¶
func (d *DelegatedUsage) Report(stats RunStats)
Report folds one finished delegated run into the accumulator. stats are the child's own totals, which already include whatever it delegated in turn, so a chain of delegations arrives here once rather than at every level.
type Event ¶
type Event struct {
Kind EventKind
// EventIterStart, EventLLMStart, EventLLMEnd, EventUserInput, EventUserInputBlocked
Iter int
// EventLLMDelta, EventLLMEnd, EventUserInput, EventUserInputBlocked
Content string
// EventLLMEnd
HasToolCalls bool
// EventLLMStart, EventLLMEnd
ContextTokens int
ContextWindow int
PromptTokens int
CompletionTokens int
// CacheReadTokens and CacheWriteTokens are the run's cached prompt so far.
// They are a breakdown of PromptTokens, not an addition to it.
CacheReadTokens int
CacheWriteTokens int
// EventLLMEnd
//
// The counts above are the run's totals so far; these are what this one
// call did. Both are carried because they answer different questions —
// what the run has spent, and which turn spent it — and deriving the
// second by subtracting consecutive records is a trap for anyone reading a
// trace where a call failed in between.
CallUsage llm.Usage
// CallCost is what this call is estimated to have cost, nil when the model
// was unpriced. Zero would read as a free call.
CallCost *llm.Cost
// EventToolStart, EventToolEnd, EventToolDenied
ToolName string
ToolCallID string
ToolArgs string // JSON-encoded arguments
// EventToolEnd
ToolResult string
ToolDuration time.Duration
// ToolErrorKind names how the call failed, empty when it did not.
//
// It reports a call that could not complete, not a task that went badly.
// A tool that ran and reported a bad outcome — a command exiting non-zero,
// a search matching nothing — succeeded at this boundary, and reading this
// field as a failure rate would flatter exactly the runs that are going
// worst.
ToolErrorKind string
// EventToolDenied, EventUserInputBlocked
DenyReason string
// EventContextCompacted
//
// CallUsage and CallCost above are set here too: compaction is a model
// call the run paid for, so it is priced like any other.
Summarized int
Kept int
// EventRunEnd
Stats RunStats
Err error // nil on success or graceful cancellation
}
Event is a structured runtime event emitted by RunLoop. Passed by value to EventSink; fields not relevant to a given Kind are zero.
type EventKind ¶
type EventKind uint8
EventKind identifies the type of a runtime event emitted during a RunLoop execution.
const ( // EventIterStart fires at the top of each agent loop iteration. EventIterStart EventKind = iota // EventLLMStart fires just before an LLM API call is dispatched. EventLLMStart // EventLLMDelta fires for each content delta when streaming is active. // Only emitted when StreamSink is also set (streaming mode). EventLLMDelta // EventLLMEnd fires after the LLM call returns with the full content // and whether the response contains tool calls. EventLLMEnd // EventToolStart fires before a tool call is evaluated (allow, deny, or execute). EventToolStart // EventToolEnd fires after a tool executes — whether it returned a result or an error string. EventToolEnd // EventToolDenied fires when a tool call is blocked before execution. EventToolDenied // EventContextCompacted fires when the message history is compacted to free context space. EventContextCompacted // EventRunEnd fires when RunLoop exits, whether successfully, on error, or on cancellation. EventRunEnd // EventUserInput fires when a message the user submitted while the run was // working is appended to the history at an iteration boundary. Content holds // the message. EventUserInput // EventUserInputBlocked fires when a UserPromptSubmit hook refuses such a // message. Content holds the message and DenyReason the hook's reason; the // message is not appended to the history. EventUserInputBlocked )
type HistoryProjection ¶
type HistoryProjection struct {
CompactionPresent bool `json:"compaction_present"`
Chars int `json:"chars,omitempty"`
}
HistoryProjection reports whether a compaction summary stood in for messages this run no longer holds, and how large it was. The journal remains the authority on what actually happened; this says only that the model was reading a lossy view of it.
type HookDecision ¶
type HookDecision string
HookDecision is the gate signal a hook may return for PreToolUse / PreCompact.
const ( // HookDecisionAllow is the zero value and means "do not interfere with the action". HookDecisionAllow HookDecision = "" // HookDecisionBlock halts the upcoming action; Reason is surfaced to the agent and user. HookDecisionBlock HookDecision = "block" )
type HookEvent ¶
type HookEvent string
HookEvent names the lifecycle point at which a hook fires.
String form is what hook implementations and persisted settings use to address an event. The values intentionally match the spelling used by Claude Code's hook design so external scripts can be reused with minimal change.
const ( // HookSessionStart fires when a session is opened or resumed. Advisory. HookSessionStart HookEvent = "SessionStart" // HookSessionEnd fires when a session is finalized/closed. Advisory. HookSessionEnd HookEvent = "SessionEnd" // HookUserPromptSubmit fires when a user prompt enters the agent, before // it is appended to history or sent to the LLM. Hooks may block to // abort the turn with a reason surfaced back to the user. HookUserPromptSubmit HookEvent = "UserPromptSubmit" // HookPreToolUse fires just before a tool would execute. Hooks may // block to deny the call. HookPreToolUse HookEvent = "PreToolUse" // HookPostToolUse fires after a tool finishes successfully. Advisory. HookPostToolUse HookEvent = "PostToolUse" // HookPostToolUseFailure fires after a tool finishes with an error. // Advisory. Separated from HookPostToolUse so audit/notification hooks // can subscribe to one channel without filtering payloads. HookPostToolUseFailure HookEvent = "PostToolUseFailure" // HookNotification fires when the agent needs the user's attention // (e.g. an approval prompt, or after a permission denial). Advisory. HookNotification HookEvent = "Notification" // HookPreCompact fires before context compaction summarizes old // messages. Hooks may block to skip compaction. HookPreCompact HookEvent = "PreCompact" // HookPostCompact fires after context compaction successfully replaces // old messages. Advisory. HookPostCompact HookEvent = "PostCompact" // HookSubagentStart fires when a subagent starts executing. Advisory. HookSubagentStart HookEvent = "SubagentStart" // HookSubagentStop fires when a subagent finishes successfully. // Advisory. HookSubagentStop HookEvent = "SubagentStop" // HookStop fires when the main agent loop finishes successfully. // Advisory. For subagents, HookSubagentStop is used instead. HookStop HookEvent = "Stop" // HookStopFailure fires when the agent loop exits with an error // (whether main agent or subagent). Advisory. HookStopFailure HookEvent = "StopFailure" // HookWorktreeCreate fires after a worktree has been created and entered. // Advisory: the tool call that asked for it already passed PreToolUse, and // a second gate over the same decision would only be a way to half-create // one. HookWorktreeCreate HookEvent = "WorktreeCreate" // HookWorktreeRemove fires after a worktree and its branch are gone. // Advisory, and for the same reason. HookWorktreeRemove HookEvent = "WorktreeRemove" // HookCwdChanged fires whenever the session's workspace root moves, // including the move a create performs. Advisory. It is the event to // subscribe to for "where is this session working now"; the two worktree // events say what happened to the tree itself. HookCwdChanged HookEvent = "CwdChanged" )
type HookInput ¶
type HookInput struct {
Event HookEvent `json:"event"`
SessionID string `json:"session_id,omitempty"`
Workspace string `json:"workspace,omitempty"`
// IsSubagent is true when the event fires inside a subagent run.
// AgentType identifies which subagent (its def name). Both fields are
// stamped on every event from a subagent so audit hooks can attribute.
IsSubagent bool `json:"is_subagent,omitempty"`
AgentType string `json:"agent_type,omitempty"`
// Populated for HookUserPromptSubmit.
Prompt string `json:"prompt,omitempty"`
// Populated for HookPreToolUse, HookPostToolUse, HookPostToolUseFailure,
// and HookNotification (when tied to an approval flow).
ToolName string `json:"tool_name,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolArgs map[string]any `json:"tool_args,omitempty"`
// Populated for HookPostToolUse (result string) and
// HookPostToolUseFailure (error string).
ToolResult string `json:"tool_result,omitempty"`
ToolError string `json:"tool_error,omitempty"`
// Populated for HookNotification: see NotificationApprovalRequired /
// NotificationPermissionDenied. NotificationReason carries any extra
// human-readable context.
NotificationKind string `json:"notification_kind,omitempty"`
NotificationReason string `json:"notification_reason,omitempty"`
// Populated for HookPreCompact (about-to-summarize counts) and
// HookPostCompact (final counts plus summary).
Summarized int `json:"summarized,omitempty"`
Kept int `json:"kept,omitempty"`
Summary string `json:"summary,omitempty"`
// Populated for HookStop, HookSubagentStop, HookStopFailure, and
// HookSessionEnd.
Stats *RunStats `json:"stats,omitempty"`
// Populated for HookStopFailure (the failure message).
Error string `json:"error,omitempty"`
// Populated for HookWorktreeCreate, HookWorktreeRemove, and
// HookCwdChanged. Workspace carries where the session is now; these say
// which tree it is and, for a move, where it came from.
WorktreePath string `json:"worktree_path,omitempty"`
WorktreeBranch string `json:"worktree_branch,omitempty"`
PreviousWorkspace string `json:"previous_workspace,omitempty"`
// Sandbox is the runtime sandbox snapshot for the current run.
// Populated on HookSessionStart (always) and on every gating event
// thereafter so hooks can enforce policy like "fail if the sandbox
// is off on the worker" without having to read settings themselves.
Sandbox *SandboxInfo `json:"sandbox,omitempty"`
}
HookInput carries the payload sent to hooks for one event.
All fields use snake_case JSON tags so the on-stdin JSON matches the project's persistence convention (CLAUDE.md §6.1). A hook implementation only reads the fields relevant to its event; the rest are zero values.
type HookOutput ¶
type HookOutput struct {
Decision HookDecision `json:"decision,omitempty"`
Reason string `json:"reason,omitempty"`
}
HookOutput is the aggregated result of running all hooks for one event.
For advisory events (PostToolUse, PostCompact, RunEnd), Decision is ignored. For gating events (PreToolUse, PreCompact), Decision == HookDecisionBlock halts the action.
func (HookOutput) Blocked ¶
func (o HookOutput) Blocked() bool
Blocked reports whether the output asks the caller to stop the upcoming action.
type HookRunner ¶
type HookRunner interface {
Run(ctx context.Context, in HookInput) HookOutput
}
HookRunner runs configured hooks for one event and returns the aggregated decision.
Implementations must be safe to call from the RunLoop goroutine and should not block indefinitely; the shell runner is expected to enforce per-hook timeouts internally. Failures inside an implementation should fail open (return HookOutput{}) and log, so a broken hook never silently breaks the agent loop.
type MemoryBody ¶
MemoryBody is one memory as the read tool returns it.
type MemoryIndex ¶
type MemoryIndex struct {
ScopeID string
Entries []MemoryIndexEntry
}
MemoryIndex is what a run carries on every model call.
type MemoryIndexEntry ¶
MemoryIndexEntry is one line of the resident index: a name to read by, and a one-clause hook saying whether it is worth reading.
type MemorySourceInfo ¶
type MemorySourceInfo struct {
Name string `json:"name"`
Revision int `json:"revision,omitempty"`
Digest string `json:"digest,omitempty"`
Chars int `json:"chars,omitempty"`
Entries int `json:"entries,omitempty"`
}
MemorySourceInfo identifies one memory source without quoting it. Revision and Digest are set for a versioned document; Entries for a counted list.
type MemoryStore ¶
type MemoryStore interface {
// Index is called on every model call, so another session's committed
// write is visible on the next iteration rather than at the end of the run.
Index() MemoryIndex
// Read returns the named bodies and, separately, the names that do not
// exist -- a missing name is an answer, not a failed call. The
// implementation records what it returned, which is what lets Write refuse
// a replacement whose body this run has never seen.
Read(ctx context.Context, names []string) ([]MemoryBody, []string, error)
// Write creates or replaces one memory. Creating a name that does not exist
// is always accepted; replacing one requires that this run read it.
Write(ctx context.Context, upsert MemoryUpsert) (MemoryBody, error)
// Delete removes one memory.
Delete(ctx context.Context, name string) error
}
MemoryStore is the seam between the loop and whatever owns the memories.
A run with no store -- a worker, an evaluation, a subagent, a session whose user turned memory off -- renders nothing and registers no tools.
func MemoryStoreFromContext ¶
func MemoryStoreFromContext(ctx context.Context) (MemoryStore, bool)
MemoryStoreFromContext returns the store for this run, if it has one.
type MemoryUpsert ¶
type MemoryUpsert struct {
Name string
Description string
Type string
Body string
// VerifiedAt is the date, as YYYY-MM-DD, that a memory caching something
// expensive was last checked against the source its body names. Empty
// leaves an existing date alone: rewording is not re-verifying.
VerifiedAt string
}
MemoryUpsert creates or replaces one memory.
It carries no version token. The store compares what this run has already read, and that comparison never leaves the runtime: routing a correctness token out to the least reliable component in the loop and expecting it back verbatim would add an omitted-parameter case whose only plausible fallback is an unconditional overwrite.
type MessageHistory ¶
MessageHistory is the minimal interface for the agent loop: read the conversation so far and append one message. The loop uses it so the same logic works with in-memory session or DB-backed conversation.
type MessageQueue ¶
type MessageQueue struct {
// contains filtered or unexported fields
}
MessageQueue holds user messages that arrived while a run was in flight, in the order they were submitted. Surfaces (CLI/TUI, Desktop, Portal) own one queue per conversation and drain it at their turn boundary; the queue itself has no opinion about when that is.
A cancelled run drops the queue: the messages behind it were written for a run the user has since abandoned, and delivering them afterwards would be a surprise. Callers do that explicitly through Drop.
Safe for concurrent use.
func NewMessageQueue ¶
func NewMessageQueue(max int) *MessageQueue
NewMessageQueue returns an empty queue holding at most max messages. A max of zero or less uses DefaultMaxQueuedMessages.
func (*MessageQueue) Dequeue ¶
func (q *MessageQueue) Dequeue() (string, bool)
Dequeue removes and returns the oldest message. The bool is false when empty.
func (*MessageQueue) Drop ¶
func (q *MessageQueue) Drop() int
Drop clears the queue and returns how many messages were discarded.
func (*MessageQueue) DropLast ¶
func (q *MessageQueue) DropLast() (string, bool)
DropLast removes and returns the most recently queued message, which is what an "undo" on the input reaches for. The bool is false when empty.
func (*MessageQueue) Enqueue ¶
func (q *MessageQueue) Enqueue(text string) (int, error)
Enqueue appends text and returns its 1-based position in the queue. It returns ErrQueueFull when the queue is at its cap, leaving the queue unchanged.
func (*MessageQueue) Len ¶
func (q *MessageQueue) Len() int
Len returns how many messages are waiting.
func (*MessageQueue) Snapshot ¶
func (q *MessageQueue) Snapshot() []string
Snapshot returns a copy of the waiting messages, oldest first, for display.
type NoopSandbox ¶
type NoopSandbox struct{}
NoopSandbox is a SandboxView whose Enabled() is always false. It is the default when no sandbox is configured; tools that hold a NoopSandbox behave exactly as they did before the sandbox subsystem existed.
func (NoopSandbox) AllowUnsandboxed ¶
func (NoopSandbox) AllowUnsandboxed() bool
AllowUnsandboxed returns true — there's no sandbox to opt out of.
func (NoopSandbox) Backend ¶
func (NoopSandbox) Backend() string
Backend returns "none" because no OS backend is providing isolation.
func (NoopSandbox) ChildEnv ¶
func (NoopSandbox) ChildEnv() []string
ChildEnv returns nil — no env injection when the sandbox is inactive.
func (NoopSandbox) Enabled ¶
func (NoopSandbox) Enabled() bool
Enabled returns false. NoopSandbox represents "sandbox subsystem is inactive on this run" — tools should fall back to pre-sandbox behavior.
func (NoopSandbox) HostAllowed ¶
func (NoopSandbox) HostAllowed(_ string) (bool, string)
HostAllowed always returns (true, "") — no enforcement when the sandbox is inactive.
func (NoopSandbox) Mode ¶
func (NoopSandbox) Mode() string
Mode returns "" because no sandbox is active.
func (NoopSandbox) ProxyAddress ¶
func (NoopSandbox) ProxyAddress() string
ProxyAddress returns "" — NoopSandbox runs no proxy.
func (NoopSandbox) ScrubEnv ¶
func (NoopSandbox) ScrubEnv(env []string) []string
ScrubEnv returns env unchanged — no scrubbing when the sandbox is inactive.
func (NoopSandbox) ShouldSandboxCommand ¶
func (NoopSandbox) ShouldSandboxCommand(_ string) bool
ShouldSandboxCommand always returns false when the sandbox is inactive.
func (NoopSandbox) WrapBashCommand ¶
WrapBashCommand returns ("", nil, nil) — the caller falls back to its own default invocation, leaving today's behavior unchanged.
type Note ¶
type Note struct {
Text string `json:"text"`
// WrittenIteration is the loop iteration at which this entry first appeared. Rewriting the list
// with the same text does not reset it, so the value reflects the entry's age rather than
// the age of the last write.
WrittenIteration int `json:"written_iteration,omitempty"`
}
Note is one durable session note.
func StampNotes ¶
StampNotes carries WrittenIteration across a rewrite: an entry whose text already exists keeps the iteration it first appeared at, and everything else is stamped with iter.
type NoteStore ¶
type NoteStore interface {
Notes() []Note
// SetNotes replaces the stored notes. Entries whose text is unchanged keep their original
// WrittenIteration; iter stamps the rest.
//
// It returns an error because a durable store commits here, and durable
// state that silently failed to commit is worse than one that says so: the
// next turn would render notes the file does not have.
SetNotes(notes []Note, iter int) error
Todos() []Todo
// SetTodos replaces the stored todos. Entries whose content and status are both unchanged
// keep their original WrittenIteration; iter stamps the rest, so a status change restarts the clock.
SetTodos(todos []Todo, iter int) error
}
NoteStore is the write side of durable session state. Tools reach it through the context rather than through a constructor, because the tool registry is cached per model and shared across sessions — a tool holding a session pointer would leak one session's state into another.
type NotesHistory ¶
type NotesHistory interface {
MessageHistory
NoteStore
}
NotesHistory is an optional extension of MessageHistory implemented by histories that carry durable session state, mirroring how CompactionHistory extends it for the compaction boundary. RunLoop renders whatever it finds here after the message list.
type PendingInput ¶
type PendingInput interface {
// Dequeue removes and returns the oldest waiting message. The bool is false
// when nothing is waiting; RunLoop calls it until it reports false.
Dequeue() (string, bool)
}
PendingInput supplies messages the user submitted while a run was already working. RunLoop drains it at the top of every iteration, where the previous iteration's tool results are complete and a user message can be appended without breaking the assistant(tool_calls) to tool pairing.
*MessageQueue implements it. A surface that would rather hand queued messages to a fresh run leaves RunLoopOpts.PendingInput nil and drains its queue itself.
type PromptLayer ¶
PromptLayer names one contributor to a run system prompt and how large it was.
The layers are what the agent was told before the conversation started, and trust-harness section 3.6 requires a run to be able to say which of them it loaded. That visibility is also what makes last-writer-wins safe for the additional system prompt: an identity change is observable afterwards rather than something an error has to prevent up front.
type RunLoopOpts ¶
type RunLoopOpts struct {
LLMClient llm.LLMClient
SystemPrompt string
// Pricing prices each call as it completes. Zero leaves every cost nil,
// which is what a model nobody priced — or a managed one, where the server
// holds the rates and records what it charged — reports.
//
// It is priced here rather than after the run because a run is where the
// rates are known to still be the ones that applied: recomputing later from
// whatever is configured then restates money already spent.
Pricing llm.Pricing
ToolRegistry llm.ToolRegistry
// MaxIter caps how many times the loop may call the model. The caller
// settles it — config.ResolveMaxIterations is where a surface gets one —
// because the bound belongs to the run being asked for, not to the loop.
// Zero runs nothing and reports the cap as exceeded.
MaxIter int
History MessageHistory
StreamSink llm.StreamSink
// Policy is consulted before each tool execution. Nil defaults to AllowAllPolicy().
Policy ToolPolicy
// Approval is invoked when Policy returns ToolActionAsk.
// Nil approval with ToolActionAsk denies the call: an unattended surface
// must not widen Ask into Allow.
Approval ApprovalHandler
// PendingInput carries messages the user submitted after this run started.
// They are appended to the history at the next iteration boundary, so they
// reach the model after the current batch of tool calls rather than after the
// whole run. Nil disables mid-run injection entirely.
PendingInput PendingInput
// Grants holds approvals the user chose to keep for the session. It is owned
// by the caller because a session outlives one RunLoop; nil grants nothing,
// which makes every Ask a fresh prompt.
Grants *SessionGrants
// MaxParallelTools bounds how many calls from one assistant message may be
// grouped to run together. Zero or one keeps every call in its own group,
// which is the sequential behaviour and the current default on every
// surface. See docs/design/parallel-tool-execution.md.
MaxParallelTools int
// Compactor summarizes old messages when the context window is filling up.
// Nil disables compaction; TrimHistory is used as a fallback.
Compactor ContextCompactor
// Checkpointer is given one turn to save durable state before a compaction discards
// messages. Nil skips the checkpoint; compaction is unaffected either way.
Checkpointer StateCheckpointer
// Invariants is the hard-constraint section of the run's additional system prompt, restated
// after the message list on every call. That text is already in SystemPrompt and never
// leaves it; this is about proximity, not storage, so it carries only the part the author
// marked as non-negotiable. Empty is the normal case.
Invariants string
// Memory supplies the resident index of cross-session recall, rendered
// after the message list and before the session-state anchor. Nil is the
// normal case for a run that has no such scope -- a worker, an evaluation,
// a subagent, a session whose user turned memory off -- and costs nothing.
Memory MemoryStore
// EventSink receives structured runtime events from the agent loop.
// Nil disables event emission entirely (zero overhead).
// The callback may be invoked from the RunLoop goroutine or from a tool
// worker. The runtime serialises the calls, so a sink sees one event at a
// time, but it must not block and must not assume one tool is in flight:
// pair EventToolStart with EventToolEnd by ToolCallID, not by arrival.
EventSink func(Event)
// Hooks runs lifecycle hooks at fixed points (PreToolUse, PostToolUse,
// PostToolUseFailure, Notification, PreCompact, PostCompact, Stop /
// SubagentStop / StopFailure). Nil disables hooks.
// PreToolUse and PreCompact hooks may block their respective actions.
Hooks HookRunner
// SessionID is forwarded to hook payloads so external scripts can correlate runs.
// Optional; an empty value is omitted from hook input.
SessionID string
// Workspace is forwarded to hook payloads so external scripts can locate files
// under the active workspace. Optional.
Workspace string
// IsSubagent is true when this RunLoop is a subagent execution. It
// flips the lifecycle event on success from Stop to SubagentStop and
// is stamped on every event from this run for audit attribution.
IsSubagent bool
// AgentType is the subagent definition name when IsSubagent is true.
// Empty for main-agent runs.
AgentType string
// RedactResult removes a run's materialized Space Secret values from a tool
// result before it enters the model context, the trace, the hooks, and the
// application log. It is a plain function so this package depends on no
// redactor; agentapp supplies one built from the run's grant values. Nil
// leaves results unredacted, which is what every surface with no Secret
// grants passes. See docs/design/space-secrets.md §12.
RedactResult func(string) string
}
RunLoopOpts configures a single run of the shared agent loop (used by both CLI agent and conversation).
type RunStats ¶
type RunStats struct {
ToolCalls int
PromptTokens int
CompletionTokens int
CacheReadTokens int
CacheWriteTokens int
// Cost is the run's estimated spend, nil when no call in it could be
// priced. CostIncomplete says a call did work that could not be priced, so
// the total understates the run rather than covering it.
Cost *llm.Cost
CostIncomplete bool
// Delegated is what subagent runs this one started spent. It is a
// breakdown of the token and cost fields above, not an addition to them:
// the totals are what the run cost, whoever executed the calls. Nil when
// the run delegated nothing.
Delegated *DelegatedStats
}
RunStats holds statistics collected during a single agent run.
CacheReadTokens and CacheWriteTokens break PromptTokens down rather than add to it, matching llm.Usage. A surface that sums all three reports a run that read more prompt than it sent.
func RunLoop ¶
RunLoop runs the LLM loop once: build messages from history, call LLM, handle tool_calls, append to history, repeat until final reply. It is used by Agent.processLoop (with session history) and by conversation.Run (with DB-backed history). When ctx is cancelled mid-run, RunLoop returns the last assistant content produced (if any) and a nil error, so callers receive a partial result rather than an empty failure.
type SandboxInfo ¶
type SandboxInfo struct {
Enabled bool `json:"enabled,omitempty"`
Mode string `json:"mode,omitempty"`
Backend string `json:"backend,omitempty"`
Sources []string `json:"sources,omitempty"`
Downgraded bool `json:"downgraded,omitempty"`
}
SandboxInfo is the snapshot of sandbox state stamped onto HookInput so hooks can attribute and policy-check without reading settings themselves.
Populated by RunLoop from the active SandboxView (see Phase E). Fields are zero-value when the sandbox is inactive.
type SandboxView ¶
type SandboxView interface {
// Enabled reports whether the sandbox is active for this run. When
// false, tools should fall back to current (pre-sandbox) behavior.
Enabled() bool
// Mode is "auto_allow" or "regular". See docs/design/sandbox-boundaries.md §5.
// Returns "" when Enabled() is false.
Mode() string
// Backend identifies the OS backend providing isolation
// ("seatbelt", "bwrap", "none"). Returns "none" when the sandbox
// is enabled in settings but the OS backend is unavailable.
Backend() string
// WrapBashCommand returns the (binary, argv) the Bash tool should
// exec to run the given command isolated by the active backend.
// `shell` is the inner shell the backend should invoke
// (e.g. "/bin/bash"); on unsupported platforms or when not wrapping
// the caller should fall back to its own default invocation.
//
// When (name == "" && err == nil) the caller must run the command
// unwrapped. This is how NoopSandbox signals "do nothing." Phase B
// fills in the real wrap; Phase A only ships NoopSandbox.
//
// ctx allows cancellation of any backend preparation (writing a
// Seatbelt profile to disk, etc.).
WrapBashCommand(ctx context.Context, command, shell string) (name string, args []string, err error)
// ShouldSandboxCommand reports whether the command should be wrapped.
// Honors the excluded_commands list (commands the user has opted
// out of the sandbox). Returns false when Enabled() is false.
ShouldSandboxCommand(command string) bool
// HostAllowed reports whether outbound network requests to host are
// permitted by the sandbox policy, plus a short reason when denied.
// When Enabled() is false this returns (true, "") — no enforcement.
//
// Non-bash tools (WebFetch, the http hook driver) consult this
// before issuing requests so the same allow-list governs every
// network egress path in the agent. The bash sandbox enforces the
// same policy at the OS level via the in-process proxy.
HostAllowed(host string) (ok bool, reason string)
// ProxyAddress returns the listen address of the in-process HTTP
// proxy ("127.0.0.1:<port>") when one is running, otherwise "".
// Surfaces in `buildmax sandbox status`; backends use it to direct
// child processes via HTTP_PROXY env.
ProxyAddress() string
// ScrubEnv returns env with secret-shaped variables removed. Applied
// by the Bash tool before composing the sandboxed child env so
// agent-process secrets (API keys, tokens, worker auth) do not
// leak into untrusted subprocesses. Returns input unchanged when
// Enabled() is false.
ScrubEnv(env []string) []string
// AllowUnsandboxed reports whether the per-call
// `dangerously_disable_sandbox` arg is honored. When false ("strict
// sandbox mode"), the arg is ignored and the call is wrapped
// regardless. Mirrors Claude Code's allowUnsandboxedCommands.
AllowUnsandboxed() bool
// ChildEnv returns env-var "KEY=VALUE" entries the caller should add
// to spawned child processes when the sandbox is active. Includes
// HTTP_PROXY routing so cooperating tools (curl, wget, git http)
// reach the network through the host-filtered proxy. Returns nil
// when no injection is needed. Unlike backend-specific env knobs
// (bwrap --setenv, sandbox-exec -D), this entry point works on every
// platform because the bash tool sets cmd.Env itself.
ChildEnv() []string
}
SandboxView is the read-only contract tools see for sandbox state.
Mirrors the design in docs/design/sandbox-boundaries.md: the sandbox isolates Bash subprocesses; non-bash tools (Read/Write/Edit/etc.) use the existing permission system, not this contract.
Implementations live in internal/infra/sandbox (Phase B). The Bash tool and the command hook transport depend on this contract; nothing in core/agent should import the implementation.
SandboxView intentionally exposes no enforcement primitives in Phase A. Phase B adds WrapBashCommand and ShouldSandboxCommand; Phase D adds the per-call dangerously_disable_sandbox escape hatch.
type SessionGrants ¶
type SessionGrants struct {
// contains filtered or unexported fields
}
SessionGrants records approvals a user chose to keep for the session.
Grants live in memory and die with the process. Persisting them means writing to the user's settings on their behalf, which is a different decision with a different blast radius — see docs/design/tool-permissions.md §7.
The nil value is usable and grants nothing, so a surface that does not offer the session option needs no store.
func NewSessionGrants ¶
func NewSessionGrants() *SessionGrants
NewSessionGrants returns an empty store, suitable for one session.
func (*SessionGrants) Scopes ¶
func (g *SessionGrants) Scopes() []string
Scopes returns the granted scopes, for display and tests.
type StateCheckpointer ¶
StateCheckpointer is handed the messages a compaction is about to discard, and gets one turn to move anything still needed into durable session state before they are gone.
This exists because a tool the model has to remember to call will be forgotten exactly when context pressure is highest. Compaction is the one moment where the runtime knows information is being destroyed, so it is the runtime, not the model, that decides the checkpoint happens.
Failure is not fatal: a checkpoint that errors is logged and compaction proceeds.
type Todo ¶
type Todo struct {
Content string `json:"content"`
Status string `json:"status"`
ActiveForm string `json:"active_form,omitempty"`
// WrittenIteration is the loop iteration at which this entry last changed status. It is what
// makes "in progress for 40 iterations" reportable.
WrittenIteration int `json:"written_iteration,omitempty"`
}
Todo is one durable task-list entry.
func StampTodos ¶
StampTodos carries WrittenIteration across a rewrite. The key is content plus status, so moving an entry to in_progress restarts its clock — which is the number worth reporting.
type ToolBoundaryHistory ¶
type ToolBoundaryHistory interface {
MessageHistory
// ToolExecutionStarted records that these approved calls are about to run.
// It must not return until that record is durable, because everything it
// is for depends on surviving the tool it precedes.
ToolExecutionStarted(calls []ToolCallStart) error
// AppendToolResult records one call's observed outcome and projects it into
// the conversation, replacing the tool-role Append a plain history takes.
AppendToolResult(out ToolOutcome) error
}
ToolBoundaryHistory is an optional extension of MessageHistory implemented by histories durable enough to record when a call crossed into its tool, in the same way CompactionHistory extends it for the compaction boundary.
The distinction it buys is the one an interrupted run cannot otherwise make. An assistant tool call proves only that the model asked. Without a record written before the tool ran, a crash leaves no way to tell a call that never started from one that may already have changed the world, and a resumed run would have to either retry it — possibly twice — or drop it. See docs/design/local-session-storage.md §7.3.
A history that does not implement this still works: RunLoop falls back to appending the tool-role message alone, which is what an in-memory history wants and all any of them can honestly offer.
type ToolCallStart ¶
ToolCallStart identifies one approved call that is about to enter its tool.
type ToolOutcome ¶
type ToolOutcome struct {
ID string
Name string
Status string
Result string
Parts []llm.ContentPart
}
ToolOutcome is one call's observed result.
type ToolPolicy ¶
ToolPolicy is the configured override layer: what a user asked for, ahead of what a tool declares about itself.
The bool is whether the policy has an opinion at all. It exists because ToolActionAllow cannot carry that distinction: at every other layer Allow means "abstain, keep resolving", so a policy that returned it could never say "allow this, stop asking" — which is the whole point of configuring one.
scope is the call's target when the tool dispatches somewhere (see grantScope), so a rule can name one MCP tool rather than every one.
func AllowAllPolicy ¶
func AllowAllPolicy() ToolPolicy
AllowAllPolicy returns a policy that defers every decision to the tool's own declaration.