Documentation
¶
Overview ¶
Package agent provides the core AI agent logic: task planning, tool invocation, and conversation.
Index ¶
- Constants
- 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 TrimHistory(msgs []llm.Message, systemTokens, contextWindow, reserveTokens int) []llm.Message
- type ApprovalHandler
- type CompactionHistory
- type ContextCompactor
- type Event
- type EventKind
- type HookDecision
- type HookEvent
- type HookInput
- type HookOutput
- type HookRunner
- type MessageHistory
- 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 RunLoopOpts
- type RunStats
- type SandboxInfo
- type SandboxView
- type ToolPolicy
Constants ¶
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 DefaultMaxIterations = 200
DefaultMaxIterations is the default cap on agent loop iterations.
Variables ¶
This section is empty.
Functions ¶
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 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.
Types ¶
type ApprovalHandler ¶
ApprovalHandler is invoked when the resolved action is ToolActionAsk. Returns true to allow execution, false to deny. If nil, Ask collapses to Deny.
type CompactionHistory ¶
type CompactionHistory interface {
MessageHistory
// AddCompaction advances the compaction boundary by summarizedCount messages and stores summary.
AddCompaction(summary string, summarizedCount int)
}
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.
type ContextCompactor ¶
type ContextCompactor interface {
Compact(ctx context.Context, msgs []llm.Message) (summary string, 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 Event ¶
type Event struct {
Kind EventKind
// EventIterStart, EventLLMStart, EventLLMEnd
Iter int
// EventLLMDelta, EventLLMEnd
Content string
// EventLLMEnd
HasToolCalls bool
// EventLLMStart, EventLLMEnd
ContextTokens int
ContextWindow int
PromptTokens int
CompletionTokens int
// EventToolStart, EventToolEnd, EventToolDenied
ToolName string
ToolCallID string
ToolArgs string // JSON-encoded arguments
// EventToolEnd
ToolResult string
ToolDuration time.Duration
// EventToolDenied
DenyReason string
// EventContextCompacted
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 )
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" )
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"`
// 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.
var NoopHookRunner HookRunner = noopHookRunner{}
NoopHookRunner is the default when no runner is configured. It allows everything.
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 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 RunLoopOpts ¶
type RunLoopOpts struct {
LLMClient llm.LLMClient
SystemPrompt string
ToolRegistry llm.ToolRegistry
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 falls through to Allow for backward compatibility.
Approval ApprovalHandler
// Compactor summarizes old messages when the context window is filling up.
// Nil disables compaction; TrimHistory is used as a fallback.
Compactor ContextCompactor
// EventSink receives structured runtime events from the agent loop.
// Nil disables event emission entirely (zero overhead).
// The callback is invoked synchronously from the RunLoop goroutine; it must not block.
EventSink func(Event)
// Hooks runs lifecycle hooks at fixed points (PreToolUse, PostToolUse,
// PostToolUseFailure, Notification, PreCompact, PostCompact, Stop /
// SubagentStop / StopFailure). Nil or NoopHookRunner 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
}
RunLoopOpts configures a single run of the shared agent loop (used by both CLI agent and conversation).
type RunStats ¶
RunStats holds statistics collected during a single agent run.
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 ToolPolicy ¶
type ToolPolicy interface {
Check(name string, args map[string]any) llm.ToolAction
}
ToolPolicy is a configured override layer consulted before tool-declared checks. Returning ToolActionAllow defers the decision to the tool's own ArgChecker / PolicyProvider. Returning ToolActionDeny or ToolActionAsk overrides the tool entirely.
var AllowAllPolicy ToolPolicy = allowAll{}
AllowAllPolicy defers all decisions to each tool's own declarations.