agent

package
v0.2.0-alpha.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package agent provides the core AI agent logic: task planning, tool invocation, and conversation.

Index

Constants

View Source
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.

View Source
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.

View Source
const (
	NotificationApprovalRequired = "approval_required"
	NotificationPermissionDenied = "permission_denied"
)

NotificationKind enumerates the reasons a HookNotification event fires.

View Source
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.

View Source
const (
	TodoPending    = "pending"
	TodoInProgress = "in_progress"
	TodoCompleted  = "completed"
)

Todo statuses. These are the values a todo may carry; anything else is rejected.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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.

View Source
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.

View Source
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

func CtxMarkSubagent(ctx context.Context) context.Context

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

func CtxWithIteration(ctx context.Context, iter int) context.Context

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

func CtxWithNoteStore(ctx context.Context, s NoteStore) context.Context

CtxWithNoteStore returns a context carrying the durable state store for the current run.

func CtxWithRunID

func CtxWithRunID(ctx context.Context, runID string) context.Context

CtxWithRunID marks ctx with the identity of the run (trace run) it belongs to.

func CtxWithToolCall

func CtxWithToolCall(ctx context.Context, toolCallID string) context.Context

CtxWithToolCall marks ctx as executing the given tool call.

func CtxWithoutMemoryStore

func CtxWithoutMemoryStore(ctx context.Context) context.Context

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

func DeclaredAccess(tool llm.Tool, args map[string]any) llm.Access

DeclaredAccess returns what a tool says this call does. A tool that declares nothing is llm.AccessWrite.

func EstimateMessageTokens

func EstimateMessageTokens(m llm.Message) int

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

func EstimateTokens(msgs []llm.Message) int

EstimateTokens returns the total estimated token count for a slice of messages.

func ExecuteTool

func ExecuteTool(ctx context.Context, t llm.Tool, tc llm.ToolCall) string

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

func ExtractInvariants(systemPromptText string) string

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

func IterationFromContext(ctx context.Context) int

IterationFromContext returns the loop iteration, or 0 when it is not known.

func RenderCompactionBlock

func RenderCompactionBlock(summary string) string

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

func RenderSessionState(invariants string, notes []Note, todos []Todo) string

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

func RunIDFromCtx(ctx context.Context) string

RunIDFromCtx returns the current run's ID, or "".

func SubagentFromCtx

func SubagentFromCtx(ctx context.Context) bool

SubagentFromCtx reports whether ctx runs inside a subagent.

func ToolCallFromCtx

func ToolCallFromCtx(ctx context.Context) string

ToolCallFromCtx returns the executing tool call's ID, or "".

func TrimHistory

func TrimHistory(msgs []llm.Message, systemTokens, contextWindow, reserveTokens int) []llm.Message

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

func ValidateInstructionLayers(spaceInstructions, agentInstructions string) error

ValidateInstructionLayers applies the shared permanent-prompt budget.

func ValidateNotes

func ValidateNotes(texts []string) error

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

func ValidateTodos(todos []Todo) error

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

type MemoryBody struct {
	Name        string
	Description string
	Type        string
	Body        string
}

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

type MemoryIndexEntry struct {
	Name        string
	Description string
}

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

type MessageHistory interface {
	HistoryMessages() []llm.Message
	Append(m llm.Message) error
}

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

func (NoopSandbox) WrapBashCommand(_ context.Context, _, _ string) (string, []string, error)

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

func StampNotes(prev, next []Note, iter int) []Note

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.

func NoteStoreFromContext

func NoteStoreFromContext(ctx context.Context) (NoteStore, bool)

NoteStoreFromContext returns the store for the current run, or (nil, false) when the run has none — a subagent run, for instance, which keeps no durable state of its own.

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

type PromptLayer struct {
	Name  string `json:"name"`
	Chars int    `json:"chars"`
}

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

func RunLoop(ctx context.Context, opts RunLoopOpts) (reply string, stats RunStats, err error)

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

type StateCheckpointer interface {
	Checkpoint(ctx context.Context, discarded []llm.Message) error
}

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

func StampTodos(prev, next []Todo, iter int) []Todo

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

type ToolCallStart struct {
	ID   string
	Name string
}

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

type ToolPolicy interface {
	Check(name, scope string, args map[string]any) (llm.ToolAction, bool)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL