agent

package
v0.1.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

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

Index

Constants

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 DefaultMaxIterations = 200

DefaultMaxIterations is the default cap on agent loop iterations.

Variables

This section is empty.

Functions

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

Types

type ApprovalHandler

type ApprovalHandler interface {
	RequestApproval(name string, args map[string]any) bool
}

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

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

type RunStats struct {
	ToolCalls        int
	PromptTokens     int
	CompletionTokens int
}

RunStats holds statistics collected during a single agent run.

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

Jump to

Keyboard shortcuts

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