agent

package
v2.9.0-dev.6 Latest Latest
Warning

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

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

Documentation

Overview

Package agent wraps the Google ADK runner with sensible defaults (streaming mode, in-memory session service, app name) so consumers hit the same shape regardless of whether they're driving the agent from a one-shot CLI, a REPL, or an HTTP handler.

Multi-turn conversation history is preserved automatically when Run() is called repeatedly with the same userID + sessionID — by default ADK's session.InMemoryService accumulates events. Pass WithSessionService to plug in a durable backend (e.g. an eventlog-backed Service for SQLite/Postgres persistence + audit log + crash-resume).

Index

Constants

View Source
const (
	// MetricGenAIInvocationDuration is the ADK-schema histogram of
	// end-to-end prompt→response turn latency.
	MetricGenAIInvocationDuration = "gen_ai.agent.invocation.duration"

	// AttrGenAIAgentName carries the agent's configured name
	// (WithName; "core_agent" by default) — distinguishes the daemon
	// root agent from spawned subagents in shared dashboards.
	AttrGenAIAgentName = "gen_ai.agent.name"

	// AttrErrorType marks failed turns. Only present on error; the
	// values are the stable attach.ClassifyTurnError kind enum
	// (config_error, auth_error, rate_limited, ...), never raw
	// error text.
	AttrErrorType = "error.type"
)
View Source
const (
	MetricAgentCompactions  = "core_agent.agent.compactions"
	MetricAgentCheckpoints  = "core_agent.agent.checkpoints"
	MetricAgentSubtasks     = "core_agent.agent.subtasks"
	MetricAgentInboxPending = "core_agent.agent.inbox_pending"
	MetricWatchdogAlerts    = "core_agent.watchdog.alerts"

	// AttrMetricSessionID mirrors pkg/usage's session.id key (kept
	// local to avoid an import for one string).
	AttrMetricSessionID  = "session.id"
	AttrWatchdogSignal   = "signal"
	AttrWatchdogSeverity = "severity"
)

core_agent.* instruments for agent-lifecycle state (#338 Phase 3).

View Source
const (
	// PauseReasonOperatorInterrupt is set by InterruptAndHold — an
	// operator stopped a turn and the loop is holding for their next
	// instruction.
	PauseReasonOperatorInterrupt = "operator-interrupt"
	// PauseReasonOperatorPause is set by a plain Pause — no turn was
	// cancelled; the loop just isn't allowed to start another one.
	PauseReasonOperatorPause = "operator-pause"
)

Pause reasons stamped on PauseState.Reason. Bare strings (not a Go enum) because they cross the attach wire into the TUI and mast-web, which shouldn't need a Go type to render them.

View Source
const (
	SubtaskDefaultMaxTurns     = 5
	SubtaskDefaultMaxWallclock = 60 * time.Second
)

Defaults for SubtaskBudgets. Aimed at "narrow tool wrapper" shape (AgenticReadFile etc.); broader-research wrappers can override.

View Source
const AutoContinueOriginator = "core-agent/auto-continue"

AutoContinueOriginator is the turn-originator identity stamped on synthesized continuation turns (via Agent.InjectAs), so eventlog metadata and audit logs distinguish them from human turns. (When the note drains into one batch with a concurrently-injected human message, the last-message-caller rule makes the human the turn originator; the note text itself still marks the turn.)

View Source
const AutonomousOverlay = `` /* 738-byte string literal not displayed */

AutonomousOverlay is layer 3b — selected by WithMode(ModeAutonomous). The narrate-before-acting line is deliberately here: the eventlog/OTel trace is a runtime property of every autonomous deployment, and stated intent is what makes a burst of tool calls legible in that record. The no-clarification line is scoped to questions in OUTPUT TEXT; a deliberately installed ask channel (tools.NewAskUserTool with a live prompter) carries its own exception in its tool description.

View Source
const CheckpointEventTag = "checkpoint"

CheckpointEventTag is the value stored under session.Event.CustomMetadata["compaction"] for a task-boundary checkpoint event. Distinct from CompactionEventTag ("summary") so the audit log + telemetry can distinguish "we hit the token wall and summarized" from "the model said this task was done."

View Source
const CheckpointNoteKey = "checkpoint_note"

CheckpointNoteKey carries the operator/model-supplied task note (the `detail` arg of mark_task_done, or the operator's /done argument) on the checkpoint event's CustomMetadata. Parallel to CompactionFocusKey for compaction events.

View Source
const CompactionEventTag = "summary"

CompactionEventTag is the value stored under session.Event.CustomMetadata["compaction"] to mark an event as a compaction summary. The history-slicing path in Run scans for the most recent event carrying this marker and drops everything before it from the LLM request. See docs/context-management-design.md (Mechanism A) for the rationale.

View Source
const CompactionFocusKey = "compaction_focus"

CompactionFocusKey carries the operator-supplied focus hint (from `/compact <focus>` or Agent.Compact(ctx, focus)) on the summary event's CustomMetadata, so it survives in the audit log alongside the summary text.

View Source
const CompactionMetadataKey = "compaction"

CompactionMetadataKey is the key under which CompactionEventTag is stored on session.Event.CustomMetadata. Exported so consumers querying the audit log can find summaries deterministically.

View Source
const CoreInstruction = `` /* 1009-byte string literal not displayed */

CoreInstruction is layer 1 — the always-on core. Two items, both harness contract: the dispatch fact (read-only tools run concurrently; the runtime serializes state-mutating tools — the #460 enforcement whose landing DELETED the old edit-sequencing prompt rule, exactly per its marked exit path) and the compaction/handover contract (the paragraph spawned subagents were silently losing pre-#459).

View Source
const DefaultAppName = "core-agent"

DefaultAppName tags this process in the ADK runner. Telemetry and session stores key off this; override with WithAppName when embedding in a host that wants its own identity.

View Source
const DefaultCompactionThreshold = 0.85

DefaultCompactionThreshold is the fallback for DefaultCompactor.Threshold when no per-tier entry matches. 0.85 leaves headroom for one more full turn before hitting the actual context wall, and matches the historical universal default so frontier sessions see no behavior change.

View Source
const DefaultInstruction = CoreInstruction + "\n\n" + InteractiveOverlay

DefaultInstruction is the pre-#459 monolithic prompt, retained as a compositional alias through the v2.8.x series (deleted at the next breaking window alongside WithSystemInstructionPrefix). Close to today's semantics minus the persona and plan-sketch lines — see the disposition table in docs/system-prompt-layering-design.md.

Deprecated: build with the layer options (WithMode, WithExtraInstruction, WithUserInstruction) instead of composing against this constant.

View Source
const DefaultSchedulingInstruction = `` /* 1137-byte string literal not displayed */

DefaultSchedulingInstruction is the composable system-instruction constant for autonomous loops that have a tools.Scheduler installed (via RunAutonomous's WithScheduler option, or per-subagent via BackgroundAgentManager). It covers the cross-cutting cadence and state-persistence guidance that doesn't fit in the schedule_next_turn tool's per-call description.

Opt-in by composition — the autonomous driver does NOT inject this automatically. Recommended consumer usage:

agent.New(m,
    agent.WithInstruction(
        agent.DefaultInstruction + "\n\n" +
        agent.DefaultSchedulingInstruction + "\n\n" +
        myConsumerInstruction,
    ),
    agent.WithTools(...),
)

See docs/scheduled-monitoring-design.md for the design rationale and the matching tool-description text (Layer 1 of the steering pattern).

View Source
const GeminiParallelismQuirk = `` /* 320-byte string literal not displayed */

GeminiParallelismQuirk is a layer-2 provider quirk applied to Gemini-family models (model identifier containing "gemini").

Probe evidence (dev/parallel-probe/): Gemini-3.1-pro-preview- customtools without this mandate never batched across 65 search turns; Claude models are "less affected, marginal benefit" and get no quirks. Retire this when a probe rerun shows the provider no longer needs the exhortation.

View Source
const InteractiveOverlay = `` /* 416-byte string literal not displayed */

InteractiveOverlay is layer 3a — the default mode overlay. Disposition only: a present user can redirect cheaply, so narrate before non-trivial work and ask focused questions when genuinely blocked. No tool names (tool mechanics live in tool descriptions).

View Source
const ToolResultNoOpKey = "no_op"

ToolResultNoOpKey is the reserved response key a tool sets to declare that an invocation changed nothing (#907). It sits alongside ADK's reserved "error" key and is read by exactly one consumer, watchdog.NoOpStreakSignal.

A tool opts in. The alternative — a registry of (tool, status) pairs the runtime knows to mean no-op, or matching the status prose — puts the knowledge somewhere that goes stale the first time a status string is reworded, and mark_task_done's repeat status carries a doc comment actively inviting that rewording.

The value is a claim about THIS call, not about the tool. A tool that sometimes does work and sometimes does not sets it per invocation; that is the whole shape the signal reads.

Variables

View Source
var ErrEmptySummary = errors.New("model returned no summary text")

ErrEmptySummary is the sentinel every text-less summarizer result wraps. Match on it (errors.Is) to tell "the model said nothing" apart from a transport, auth, or persistence failure — the callers that swallow a failed reduction want to report the two differently, and the difference is invisible in a wrapped error string.

View Source
var ErrInboxClosed = errors.New("agent: inbox closed")

ErrInboxClosed is returned by Agent.Inject when the agent's inbox has been closed (typically because the agent has been shut down via its run context cancelling). Callers that publish messages on an unrelated lifecycle (e.g. a stdin scanner) should treat this as "stop publishing."

View Source
var ErrNoCheckpointer = errors.New("agent: no checkpointer wired (pass WithCheckpointer at agent.New)")

ErrNoCheckpointer is returned by Agent.Checkpoint when the agent was constructed without WithCheckpointer. Callers should check for this sentinel before treating it as a hard failure.

View Source
var ErrNoCompactor = errors.New("agent: no compactor wired (pass WithCompactor at agent.New)")

ErrNoCompactor is returned by Agent.Compact when the agent was constructed without WithCompactor. Callers should check for this sentinel before treating it as a hard failure.

View Source
var ErrSideQuestionEmpty = errors.New("agent: AskSideQuestion: model returned no text")

ErrSideQuestionEmpty is the sentinel every empty side-question answer wraps. Match on it (errors.Is) to tell "the model had nothing to say" apart from a transport, auth, or API failure — the two want opposite treatments at every surface: the first is an answer of sorts and renders inline, the second is an error and renders as one.

Before this existed, /btw returned a bare errors.New for the empty case, so a thought-only response and a dead endpoint looked identical to the TUI. That is the "blank/infra error" symptom the operator reported: the response WAS empty, and the surface had no way to say so except by failing.

View Source
var ErrSubtaskSpecInvalid = errors.New("agent: invalid SubtaskSpec")

ErrSubtaskSpecInvalid is returned when SubtaskSpec fails validation (empty Name / SystemPrompt / UserMessage). Pre- flight check so the caller gets a clear error rather than a confusing downstream ADK failure.

View Source
var ErrTurnInFlight = errors.New("agent: cannot compact/checkpoint while a turn is in flight; retry after the turn completes")

ErrTurnInFlight is returned by Compact / Checkpoint when a Run turn is currently in flight on this agent. Both operations append a boundary event directly to the parent session row; doing so mid-turn races the runner's own AppendEvent (ADK optimistic-concurrency failure against the runner's stale snapshot) and can wedge a boundary between a persisted functionCall and its functionResponse, which the history-slicing path (sliceFromBoundary) would then emit as an orphaned functionResponse that Gemini rejects. Callers (AttachCompact, the TUI /compact + /done slashes) should retry once the turn ends. See #355. The internal pre-turn drivers (runPendingCompaction / runPendingCheckpoint) run before the turn's cancel is registered, so this guard never blocks the automatic threshold-driven path.

Functions

func AutoContinueNote added in v2.8.0

func AutoContinueNote(interruptedAt time.Time) string

AutoContinueNote renders the synthesized continuation prompt. It is a system note, not impersonated user text: the model is told what was DETECTED and asked to pick the task back up. It describes what the classifier actually observed — an unfinished turn — and does NOT claim a cause (e.g. a daemon restart) it cannot verify: the same note fires from lazy-resume, boot scan, startup-session, and the in-lifetime retry loop, and interruption is inferred from tail shape alone (#615). Must contain autoContinueMarker (guarded by test).

func AutoContinueNoteFor added in v2.9.0

func AutoContinueNoteFor(interruptedAt time.Time, interruptedCalls []string) string

AutoContinueNoteFor selects the continuation note appropriate to what was interrupted (#624). When the tail died mid-tool and EVERY interrupted call classifies read-only (tools.IsReadOnlyToolName), it returns the read-only variant that does NOT nudge re-issuing them; otherwise (a mutating or unknown interrupted call, or an interrupted shape with no calls — a bare user message or a committed tool response) it returns the default note, which still tells the model it MAY re-issue. Classification is by tool metadata, never a note-local name list: an unrecognized name falls to the default (keep the nudge), the conservative direction.

func ClassifyInterruptedTail added in v2.8.0

func ClassifyInterruptedTail(events []*session.Event) (interruptedAt time.Time, interrupted bool)

ClassifyInterruptedTail reports whether a session's committed history ends in an interrupted turn, and when the interruption happened (the timestamp of the last committed event of the broken turn). Detection is derived entirely from eventlog state — no write-ahead marker exists or is needed; per-event persistence means the tail shape IS the marker (docs/auto-continue-design.md §Detection). The one place tail shape is insufficient is a MAX_TOKENS output-cap truncation that still emitted text — indistinguishable from a normal completion by shape alone — so the eventlog overlay stamps the genai FinishReason into CustomMetadata and the hasText arm consults it (#582).

The classifier walks backward to the last *conversational* event on the parent branch, skipping annotation rows: subagent branches, streaming partials, contentless/role-less events (autonomous checkpoints and notes, interrupt-audit rows), and compaction summaries. That event then classifies as:

  • user message (no tool parts) → interrupted: the question was committed, no answer ever came.
  • carries a functionResponse part → interrupted: a tool result (possibly a #537 tail-repair synthesis) was committed but no model turn consumed it.
  • carries a functionCall part → interrupted mid-tool (the #537 repair target, seen before repair has run) — UNLESS every call in the event is long-running (LongRunningToolIDs): ADK treats a turn ending in only long-running calls as final, with responses legitimately arriving in a later user turn.
  • anything else (model text) → completed turn, UNLESS a stamped MAX_TOKENS finish reason marks it truncated mid-task (then: interrupted — see incompleteFinish).

KNOWN LIMITATION — "interrupted" vs "in progress" (#796). Every arm above reads committed history and nothing else, so an interrupted turn and a turn that is STILL RUNNING are the same tail: a session whose user message has committed and whose model reply has not classifies interrupted whether the generation died or is thirty seconds into a long answer. This is not fixable here — no shape in the eventlog distinguishes the two, and a time-based guess ("a user event newer than N seconds is probably still running") would be exactly that, a guess, in a position where being wrong duplicates a reply. Liveness is a fact the running process holds, not one history records, so callers that might run concurrently with a turn must consult it: check Agent.TurnInFlight before acting on an interrupted verdict (pkg/compose's lockClassifyInject does, for all three auto-continue triggers). On the boot path the ambiguity cannot arise — after a restart nothing is in flight — which is why the in-lifetime retry driver was the caller that found this.

Additional terminal shapes (never continued): an operator interrupt-audit row anywhere after the tail (deliberate kill), a TERMINAL ErrorCode final (see the ErrorCode arm and transientTurnError), an empty-parts agent-authored final (Gemini streaming can close a completed turn with an empty aggregate), a turn parked on ADK's tool-confirmation flow, a SkipSummarization response final, and a tail that IS a prior committed continuation note (one automatic attempt per interruption — the lazy-path crash-loop bound).

func ClassifyInterruptedTailWithCalls added in v2.9.0

func ClassifyInterruptedTailWithCalls(events []*session.Event) (interruptedAt time.Time, interrupted bool, interruptedCalls []string)

ClassifyInterruptedTailWithCalls is ClassifyInterruptedTail plus the tool-call names of the interrupted tail (#624). interruptedCalls is populated ONLY for a mid-tool interruption (the functionCall arm) — the shape whose continuation note carries a "re-issue interrupted tool calls" nudge — and lists every call name in that tail event. It is nil for the other interrupted shapes (a bare unanswered user message, or a committed functionResponse whose result is already in history), and nil when the tail is not interrupted. Callers scope the continuation note by classifying these names (see AutoContinueNoteFor).

func EventlogMetadataExtractor

func EventlogMetadataExtractor() eventlog.MetadataExtractor

EventlogMetadataExtractor returns an eventlog.MetadataExtractor that pulls the per-event auth.Caller identity (and proxy attribution, when present) from the request context onto the eventlog row's sidecar metadata.

Pass to eventlog.Open via eventlog.WithMetadataExtractor — typically at daemon startup in cmd/core-agent. The extractor is a pure function of the per-event context, so the eventlog package itself stays auth-agnostic; only the binary that wires both packages together depends on both.

Returns nil entries when no Caller is on context (legacy / single-user / out-of-band Run callers); the eventlog package then stores no sidecar JSON for that row.

func FormatAutoContinueInbox

func FormatAutoContinueInbox(messages []string) string

FormatAutoContinueInbox renders messages with the system-note framing used by the TUI's "auto-continue from queued input" flow (see docs/operator-input-design.md). The framing tells the model these notes arrived while it was working and gives it the explicit branches in inboxHandlingGuidance — most importantly the dedup case (operators frequently re-phrase the same ask while waiting) and the post-completion case (the prior turn ended with mark_task_done, so there's no "current task" to adapt).

Only the header is this flow's own: these notes really are an operator's, and saying so is worth a line, whereas the daemon's block carries machine signals under the stable "[Inbox]" name. Everything below the bullets is shared with the daemon path, so the two can no longer drift (#697).

Returns "" when messages is empty (caller should not auto-continue in that case). Senders are not labelled here: this header already says the notes are the operator's, so a per-bullet identity would be redundant on the one surface where provenance was never ambiguous.

func FormatInterruptContinue added in v2.9.0

func FormatInterruptContinue() string

FormatInterruptContinue frames a resume that carries no new instruction — the operator stopped the model, looked, and said carry on. Distinct from AutoContinueNoteFor because the interruption here was deliberate and human, so the model shouldn't treat the stop itself as a signal that something went wrong.

The re-check line is the load-bearing one: tail repair (#537) patches a dangling functionCall into a well-formed history, but the real-world EFFECT of a cancelled bash or write_file is genuinely unknown, and a model that assumes completion will happily build on a half-applied change.

func FormatInterruptSteer added in v2.9.0

func FormatInterruptSteer(text string) string

FormatInterruptSteer frames the instruction an operator typed in answer to "what do you want me to do instead?" after interrupting a turn. Sibling of FormatAutoContinueInbox, and deliberately blunter: auto-continue is guessing that the model should carry on, whereas here a human explicitly stopped the model and said something. The last line exists because the default failure mode of a resumed model is to quietly go back to what it was doing and mention the operator's note in passing.

Returns "" for empty text so callers can fall back to FormatInterruptContinue.

func IsCostCeilingExceeded

func IsCostCeilingExceeded(err error) bool

IsCostCeilingExceeded returns true when err was returned by Run because a previous turn tripped a configured cost ceiling. Operators / hosts use this to distinguish "operator must reset the ceiling" from other Run errors that may warrant retry.

func IsWatchdogTripped added in v2.9.0

func IsWatchdogTripped(err error) bool

IsWatchdogTripped reports whether err was returned by Run because a prior turn tripped the behavioral watchdog under --watchdog=enforce. Hosts use this to distinguish "operator must reset the watchdog" from other Run errors that may warrant retry.

func NewMarkTaskDoneTool

func NewMarkTaskDoneTool(getter func() *Agent) tool.Tool

NewMarkTaskDoneTool returns the model-facing tool that signals task completion. The handler doesn't fire the checkpoint inline — that would require synchronous LLM I/O from inside a tool call, which ADK's runner doesn't expect. Instead the handler stashes the detail on the agent and flips a pending flag; Agent.Run's post-turn hook picks it up and fires Checkpoint before the next turn.

Takes a getter rather than a *Agent directly because we register this tool BEFORE the agent struct is constructed (llmagent.New snapshots its tool list at construction time, so registration has to happen up front). The getter resolves lazily — agent.New sets the agent pointer after llmagent.New returns, and the getter walks the closure to find it. A nil return from the getter is treated as "registration race not yet completed" and the call is a silent no-op (defensive — shouldn't happen in practice because the model never sees the tool before agent.New returns).

Registered automatically in agent.New when a Checkpointer is wired via WithCheckpointer, unless WithoutMarkTaskDoneTool asks for the operator-only posture (#905).

The description is prompt text and was rewritten as such (#905/#909). The original told the model to "use this generously at natural task boundaries (after shipping a feature, finishing a code review, completing a debugging session)" — three examples from an interactive coding session, plus a frequency instruction. A daemon consuming machine signals has no conversation about to shift to a new task, so every inbox bundle after a closed incident reads as a boundary; one live deployment produced sixteen calls in a single session and answered unrelated operator questions with completion reports. Two prose layers in the recipe forbade exactly that behavior and lost, because a tool description outranks the persona at the point of decision and a recipe author cannot edit it.

So: no frequency instruction, no workload examples, and the negations name the observed failure rather than only the obvious one ("mid-task"). Skipping a boundary is stated as costless because it is — compaction reduces context on its own, and the model has no way to know that otherwise.

func NewSubagentTool

func NewSubagentTool(opts SubagentOptions) (tool.Tool, error)

NewSubagentTool wraps an *agent.Agent as a tool the parent's model can call. The subagent runs through ADK's runner using the parent's session.Service (so its events stream live into the same audit log as the parent), with session.Event.Branch set to "<parent_branch>.<this>" so the audit log stays distinguishable and ADK's contents-processor branch filter keeps the subagent's events from leaking into the parent's next-turn LLM request.

The parent's session.Service is captured from Inner — Inner is expected to have been constructed with the same WithEventLog (or WithSessionService) the parent uses. The agent.WithSubagents convenience option handles this wiring automatically; consumers who construct subagent tools directly via NewSubagentTool need to share the session.Service themselves.

func RegisterMetrics added in v2.8.0

func RegisterMetrics(mp metric.MeterProvider, src AgentSource) (metric.Registration, error)

RegisterMetrics wires the per-agent lifecycle observers against mp: compactions, checkpoints, subtasks (counters) and inbox_pending (gauge), each dimensioned by session.id. Call once at boot with the process-global MeterProvider.

Counter sources are the in-memory per-process fields on Agent — NOT the eventlog-derived ContextStats, which is an O(events) scan per call and resumes across restarts (a restart would step an ObservableCounter backward or forward arbitrarily).

func SubagentReturnContract added in v2.9.0

func SubagentReturnContract(returnTool string) string

SubagentReturnContract renders the instruction block that tells a delegated subagent its output is a value returned to another agent (#727).

returnTool is the name of the tool that hands a value back and ends the run. Pass "" for delegation paths that have no such tool — a subagent invoked synchronously as a tool returns its last message and nothing else — so the instruction never names a gesture the runtime hasn't registered.

Why this is an instruction and not a tool description. [#641] put the same framing on the done tool's description, which is only read by a model that goes looking for that tool. The contract has to hold on every termination path — a natural stop, a budget cap, a watchdog halt, a sync invocation with no done tool at all — and on all of those the subagent's *last assistant text* is what the delegating agent receives. A subagent that thinks it is writing a status update for a human writes "standing by in a healthy, inactive state"; one that knows it is returning a value writes the findings.

Install with WithExtraInstruction (layer 5), which appends whether or not the caller replaced layers 1–3 — the contract is a property of being a delegation, not of the harness baseline.

Types

type Agent

type Agent struct {
	// contains filtered or unexported fields
}

Agent is the wrapper around an ADK llmagent + runner. One Agent represents one configured LLM-driven role.

func New

func New(model adkmodel.LLM, opts ...Option) (*Agent, error)

New constructs an Agent backed by model. Returns a clear error if the underlying ADK constructors reject the configuration.

func (*Agent) AddSessionCostBudget added in v2.9.0

func (a *Agent) AddSessionCostBudget(usd float64) (CostCeiling, error)

AddSessionCostBudget raises the per-session ceiling by usd and returns the ceilings that result. This is the ONLY mutation the reset surface offers, and deliberately so: the alternatives — zeroing the accumulator, or restarting a spend "window" — would make Agent.SessionCostUSD, /usage and the eventlog-derived cost disagree about what the session actually spent. Raising the bar keeps every dollar counted and still hands the operator runway.

usd must be > 0. Raising a disabled (0) session ceiling is refused: that would silently ARM a bound the operator never configured, which is a tighter posture than they asked for, not a looser one.

func (*Agent) AgentName

func (a *Agent) AgentName() string

AgentName returns the configured agent name (the WithName value) stored on construction. Used by NewSubagentTool to derive a default tool name.

func (*Agent) AppName

func (a *Agent) AppName() string

AppName returns the AppName the agent was constructed with (the value passed to runner.Config). Used by callers that need to identify the session triple (app, user, session) for queries against the event log or session.Service.

func (*Agent) AskSideQuestion

func (a *Agent) AskSideQuestion(ctx context.Context, question string) (string, error)

AskSideQuestion runs one tool-less LLM call that sees the agent's current conversation history plus the supplied question, and returns the model's answer as a single string. Intended for the TUI's /btw side-question flow (docs/operator-input-design.md layer C): the operator asks a quick context-grounded question ("what was that file again?") without polluting the main conversation. The call bypasses Agent.Run entirely — no inbox drain, no permission gating, no event-log writeback, no tools.

The question is appended to the existing history as a transient user turn that exists only for this one call; nothing about the agent's persisted session state changes. The model can therefore reference prior tool output, prior assistant turns, prior user messages — but cannot call any tool to do new work.

Errors:

  • context cancellation: returns ctx.Err().
  • no session.Service wired: returns a clear error (defensive; agent.New always installs one, but hand-constructed Agents used in tests don't).
  • GenerateContent failures bubble up unchanged so callers can distinguish transport vs API vs model errors via errors.Is.
  • the model answering with no text: *SideQuestionEmptyError, which wraps ErrSideQuestionEmpty. Callers are expected to render it as "no answer" rather than as a failed call — see that type's doc comment.

func (*Agent) BackgroundManager

func (a *Agent) BackgroundManager() SubagentManager

BackgroundManager returns the SubagentManager the agent was constructed with via WithBackgroundManager, or nil when none was wired. Used by spawn tools + the runner's REPL alert display to reach the manager without keeping a separate reference. Callers that need the concrete *background.Manager recover it with background.ManagerOf.

func (*Agent) Checkpoint

func (a *Agent) Checkpoint(ctx context.Context, taskNote string) (CheckpointResult, error)

Checkpoint writes a task-boundary checkpoint event to the session and clears any pending checkpoint flag. Like Compact, the event becomes the slicing boundary for the next turn's model request.

taskNote is the operator/model-supplied detail (the mark_task_done `detail` arg, or /done's argument). Empty is fine — the prompt still produces a useful summary, just without the leading completion note.

Errors:

  • ErrNoCheckpointer when no checkpointer was wired.
  • Context cancellation: ctx.Err().
  • Model errors propagate wrapped so callers can errors.Is on transport vs API failures.

func (*Agent) CloseInbox added in v2.8.0

func (a *Agent) CloseInbox()

CloseInbox marks the agent's inbox closed so every subsequent Inject / InjectAs fails with ErrInboxClosed instead of queuing into a mailbox no live turn will ever drain. The wake-driven surfaces (runner.WakeLoop) call this when their loop exits — both daemon shutdown and per-session eviction cancel that loop's context — so a post-death inject that raced past handler-level gating fails loudly at the source rather than being acknowledged and silently lost (#566). Idempotent and nil-safe.

func (*Agent) Compact

func (a *Agent) Compact(ctx context.Context, focus string) (CompactionResult, error)

Compact runs an out-of-band summarizer LLM call against the current session's history and writes the result as a "summary" marker event the history-slicing path in Run picks up on the next turn. Used both programmatically and by the TUI's /compact slash. See Agent.Checkpoint for the task-boundary variant that shares this machinery.

focus is an optional hint for the summarizer ("focus on the auth-rewrite thread"). Empty is fine — the default prompt instructs the model to produce a balanced handover.

Errors:

  • ErrNoCompactor when no compactor was wired.
  • Context cancellation: ctx.Err().
  • Model errors propagate wrapped so callers can errors.Is on transport vs API failures.

func (*Agent) CompactIfNeeded

func (a *Agent) CompactIfNeeded(ctx context.Context, focus string) (CompactionResult, error)

CompactIfNeeded fires Compact when the wired Compactor's ShouldCompact returns true, otherwise reports Skipped=true. Useful for hosts that want to drive automatic compaction on a turn-end hook without re-implementing the threshold check.

func (*Agent) ContextStats

func (a *Agent) ContextStats() ContextStats

ContextStats returns a snapshot of compaction/checkpoint/subtask activity for this session. Safe to call from any goroutine. Errors fetching the session (e.g. session.Service hiccup) are swallowed and result in zero boundary fields — the subtask fields are populated regardless since they're in-memory.

Cost: one session.Service.Get() call + O(events) scan. Designed for operator-driven /context slash usage, not hot-path telemetry — cache or sample if you need it per-turn.

func (*Agent) CostCeilingLimits added in v2.9.0

func (a *Agent) CostCeilingLimits() CostCeiling

CostCeilingLimits returns the ceilings currently in force, including any runway added since construction via AddSessionCostBudget. Zero fields mean that bound is disabled.

func (*Agent) CostCeilingTripped

func (a *Agent) CostCeilingTripped() (bool, string)

CostCeilingTripped reports whether the agent is currently blocking new turns because a configured ceiling was exceeded. Exposed for /stats and similar UI surfaces; operators surface this alongside the totals so the "why is the agent refusing my prompts?" question has an obvious answer.

Returns (true, reason) when blocked; (false, "") otherwise.

func (*Agent) DelegationBudgets added in v2.9.0

func (a *Agent) DelegationBudgets() SubagentBudgets

DelegationBudgets returns the caps that bound one delegation TO this agent when a parent exposes it as a subagent tool — what WithSubagentBudgets set. Zero dimensions are unbounded.

It reads back what was declared, not what a run has spent. The asynchronous twin's caps live on its background.SubagentTemplate; cmd/core-agent fills both from one config block, and this accessor is what lets a test assert the pair did not drift.

func (*Agent) Description

func (a *Agent) Description() string

Description returns the one-line description set via WithDescription. Empty when unset. Satisfies attach.DescriptionProvider — the /.well-known/agent-card.json handler falls back to this when no explicit AgentCardConfig.Description override is supplied.

func (*Agent) DrainInbox

func (a *Agent) DrainInbox() []string

DrainInbox pulls every currently-queued message off the inbox and returns them in arrival order. The queue is emptied as a side effect — repeat calls return nil until new messages arrive.

Exposed so harnesses that want to format inbox messages differently from the built-in `[Inbox]` block (e.g. the TUI's auto-continue flow uses FormatAutoContinueInbox to add system-note framing) can pull the messages themselves and pass the formatted result to Run as a regular prompt. Run's internal drain then finds an empty inbox and is a no-op, so there's no double-injection.

Returns nil when no agent or no inbox is wired — both are defensive cases for hand-constructed Agent structs in tests.

Emits an `inbox`/dequeued event for each drained message with the message's prompt_id, so SSE consumers can correlate the dequeue with the earlier queued event.

Callers that need the per-message originator identity (multi-session turn threading) should use drainInboxFull instead (internal). The public DrainInbox shape is preserved for external harnesses that only care about the text payloads.

func (*Agent) Emit added in v2.8.0

func (a *Agent) Emit(eventType string, payload any)

Emit is the exported seam over emit() for the packages that will split out of pkg/agent (pkg/agent/background's inbox lives outside the core type — see docs/agent-package-split-design.md). It pushes a typed event onto the attach SSE stream, or is a no-op when no subscriber is connected. Safe on a nil receiver.

func (*Agent) EventLog

func (a *Agent) EventLog() *eventlog.Handle

EventLog returns the *eventlog.Handle the agent was constructed with via WithEventLog, or nil when no event log was wired. Use to reach back to Stream.Since / Stream.Watch for replay or live tail without keeping a separate reference.

func (*Agent) Gate added in v2.8.0

func (a *Agent) Gate() *permissions.Gate

Gate returns the permissions gate wired via WithGate, or nil when none was configured. Read-only seam for the split-out packages (pkg/attachadapter projects gate state onto the attach wire format); mutations go through the gate's own methods.

func (*Agent) GenerateSessionTitle added in v2.9.0

func (a *Agent) GenerateSessionTitle(ctx context.Context, prompt string) (string, error)

GenerateSessionTitle runs one tool-less LLM call that turns prompt into a short label. Exported so a host can drive titling explicitly (a backfill over existing sessions, a "retitle this" operator action) rather than only through the automatic first-turn path.

The call uses the small-model tier when one was wired via WithTitleModel and the parent model otherwise. Falling back to the parent model is safe HERE and not on the automatic path: a caller who reaches for this method has asked for a generated title and can be billed for one, whereas the first turn of every session cannot.

Unlike AskSideQuestion this deliberately does NOT see the session history: one call at session start, not a cost that recurs.

func (*Agent) HasCheckpointer

func (a *Agent) HasCheckpointer() bool

HasCheckpointer reports whether a Checkpointer was wired via WithCheckpointer. Hosts use this to gate `/done` (and the `/checkpoint` alias) out of `/help` and the slash palette when --no-checkpoint was passed. Same shape as HasCompactor.

func (*Agent) HasCompactor

func (a *Agent) HasCompactor() bool

HasCompactor reports whether a Compactor was wired via WithCompactor. Hosts use this to gate operator-facing surfaces: don't list `/compact` in `/help` when there's nothing to invoke. Same idea as nil-checking a.compactor directly, but exported so adapters living outside the agent package don't need a reflection trick.

func (*Agent) HasPendingOperatorInput added in v2.9.0

func (a *Agent) HasPendingOperatorInput() bool

HasPendingOperatorInput reports whether the inbox holds a queued message from someone OTHER than auto-continue itself — real operator/external input waiting to drive the next turn. Auto-continue consults this to STAND DOWN (#624): if an operator has already queued input (e.g. `stop`) while a turn was interrupted, that input must drive the next turn on its own — injecting a "continue the task" note into the same drained batch lets the note outrank the operator's stop (the #624 race). Messages injected by auto-continue itself (AutoContinueOriginator) are excluded so it never sees its own note as operator input. Zero-identity messages (legacy Inject / single-user) DO count — they are operator input without an attached identity.

Messages queued by QueueAsContext (#698) are excluded too, for the same reason auto-continue consults this at all: the stand-down is worth it only because the queued input is ITSELF about to drive a turn. A deferred message explicitly is not — standing down for one would leave an interrupted session with nothing to re-drive it and the deferred context undrained, which is the opposite of both features' intent. Auto-continue proceeds, and its turn drains the deferred message alongside its own note, which is exactly the batching {"wake": false} exists to get.

Returns false for nil agent / inbox.

func (*Agent) InboxArrived

func (a *Agent) InboxArrived() <-chan struct{}

InboxArrived returns a channel that fires when a new message has been injected. The harness can use this to decide when to start the next turn instead of polling — typical pattern:

for {
    select {
    case <-ctx.Done():
        return
    case <-ag.InboxArrived():
        runOneTurn("continue") // inbox prepended automatically
    }
}

The channel has a 1-buffer signal-style semantics: multiple pushes between consumer wake-ups coalesce into one notification (the consumer drains the queue and sees them all).

func (*Agent) Inject

func (a *Agent) Inject(message string) error

Inject queues message on the agent's inbox. The next call to Agent.Run will drain pending messages, format them as an "[Inbox]" block, and prepend the block to the prompt the model sees.

Inject is safe to call concurrently from any goroutine — typical usage is a harness goroutine reading from stdin, an HTTP handler, or an orchestrator's gRPC stream. Drop-oldest backpressure kicks in when the queue exceeds the soft cap (256) so a stuck consumer can't deadlock the agent.

Emits an `inbox`/queued event on the attach event stream (if one is wired) with the assigned prompt_id, so SSE consumers can surface the queued state to operators. The same prompt_id flows out as the `inbox`/dequeued event when the inbox is drained inside Run.

Returns ErrInboxClosed once the agent has been shut down; callers publishing on an unrelated lifecycle should treat that as "stop."

Inject queues without an originator identity. Callers that have a per-request auth.Caller (typically attach handlers in a multi-session deployment) should use InjectAs instead — the caller is threaded into the turn context so the eventlog metadata sidecar and per-caller MCP path see who triggered the turn.

Inject carries no trace context. Callers on an HTTP request path (the attach /inject and /wake handlers, an orchestrator's RPC server) should use InjectAsContext so the turn that answers the inject can be linked back to the injecting span.

Injecting into a PAUSED agent queues the message and leaves the gate shut (#878). The turn that eventually drains it is the one an operator releases with Resume/ResumeWith — see InjectAsContext.

func (*Agent) InjectAs

func (a *Agent) InjectAs(message string, caller auth.Caller) error

InjectAs is Inject with a per-message originator identity. The caller is stored on the queued message and used as the turn originator when the inbox drains: the agent loop wraps the turn context with auth.WithCaller(caller) so eventlog metadata, MCP outbound context, and other caller-aware substrates see the identity that triggered the turn.

Zero-value caller (Identity == "") is equivalent to Inject — no identity is threaded.

When multiple injects queue between turns, the LAST message's caller wins as the turn originator (per docs/multi-session-design.md "the turn answers the most recent ask"). Same prompt_id correlation, same SSE events as Inject.

InjectAs carries no trace context; see InjectAsContext.

func (*Agent) InjectAsContext added in v2.9.0

func (a *Agent) InjectAsContext(ctx context.Context, message string, caller auth.Caller) error

InjectAsContext is InjectAs plus the injecting call's context, so the queued message remembers the OpenTelemetry span that produced it. Everything else is identical — same queue, same prompt_id, same SSE events, same last-caller-wins originator rule.

ctx is used ONLY to read the active span context. It is deliberately not stored, not used for cancellation, and not consulted for the caller identity (pass that explicitly): an inbox message outlives the request that queued it by design, so keeping the request's context alive on the queue would be a lifetime bug.

Why a link and not a parent: the injecting request returns as soon as the message is queued, so its span has already ended by the time a turn drains the inbox — and one turn can drain SEVERAL injects, so there is no single parent to pick. The agent loop therefore attaches one span LINK per drained inject that carried a valid span context to the turn span it starts (see turnspan.go). Injects with no trace context are a clean no-op.

Injecting into a paused agent

An inject QUEUES; it does not un-park (#878). The message lands on the inbox, publishes its `inbox`/queued event, and fires the wake signal — which latches, buffered-1, so nothing is lost — and then waits behind the gate with everything else. Whatever turn the operator's Resume releases drains it, coalesced into the same `[Inbox]` block as the operator's own instruction.

Until #878 an inject from anyone but auto-continue called Resume itself, on the theory that injecting while parked IS the operator answering "what should I do instead?". That holds for a human at a keyboard and fails for everything else on the same door: auth.Caller carries no human/machine bit, and the identity a machine injects under can be a person's (k8s-lookout's watcher asserts its --owner via a proxy identity), so the runtime cannot tell them apart. An alert arriving mid-park was therefore enough to open a gate a human had deliberately shut.

Releasing a hold is now only ever done by something that says so: Resume, ResumeWithMode, or ResumeWith — reached over the wire as POST /resume. If you want the pre-#878 one-call behavior, that is ResumeWith(mode, message, caller), which queues and then opens the gate in the order that keeps the instruction ahead of the turn.

func (*Agent) InjectAsContextWithID added in v2.9.0

func (a *Agent) InjectAsContextWithID(ctx context.Context, message string, caller auth.Caller) (string, error)

InjectAsContextWithID is InjectAsContext returning the prompt_id it assigned to the queued message — the same id that goes out on the `inbox`/queued event, comes back on `inbox`/dequeued when a turn drains it, and names the turn on `turn-complete`.

It exists because the id was being computed and thrown away: an attach client that puts a chat thread in front of a session has no other way to key state on the turn its message will produce. `turn-complete.prompt_id` names it, but only at the end, and by then the client needed to have known since the start; the `inbox` events carry it but have no `seq`, so a counter driven off them desynchronises across exactly the reconnect it most needs to survive; and correlating "the queued event that just fired" with "the inject I just sent" holds right up until two goroutines inject on one session, which is the case that needs it (#840).

A sibling rather than a changed signature: pkg/agent is inside the stability promise, and InjectAsContext returning (string, error) would be a source break for every out-of-tree caller.

The id is NOT one-to-one with a turn, and callers must not treat it as one. The inbox coalesces — several messages queued between turns drain into a single "[Inbox]" block and run as one turn (see docs/multi-session-design.md, "the turn answers the most recent ask") — so N injects can share one turn, and `turn-complete` names only one of the ids. What the id buys is the ability to SEE that fan-in and collapse client state accordingly, which is not possible without it.

func (*Agent) Inner added in v2.8.0

func (a *Agent) Inner() adkagent.Agent

Inner returns the underlying ADK agent the turn loop drives. It is the read-only seam the split-out driver package (pkg/agent/autonomous, see docs/agent-package-split-design.md) uses instead of reaching the unexported field directly; returns nil if the agent is nil or was constructed without an inner agent.

func (*Agent) Interrupt

func (a *Agent) Interrupt() bool

Interrupt cancels the in-flight turn (if any) by invoking the stored cancel func. Returns true if there was something to cancel (a turn was in flight when called), false if the agent was idle (no-op). Safe for concurrent callers.

Repeatable for as long as the turn is actually alive: the cancel func stays registered until the turn's own gen-keyed clearCancelInFlight fires from cleanup, so a second Interrupt while the first is still unwinding reports true again instead of "nothing in flight". It used to clear the func itself, which meant an operator whose first cancel didn't bite promptly — a tool ignoring its context, a model call mid-retry — pressed again and was told the agent was idle while it visibly kept working. context.CancelFunc is idempotent, so the repeat cancel costs nothing.

Note this also keeps turnInFlight() true through the unwind, so Compact / Checkpoint keep refusing their mid-turn boundary writes (#355) until the turn has genuinely finished flushing — which is the window those refusals exist to protect.

Cancellation propagates through context.Canceled to the in-flight model call. The agent's tools (bash, fetch_url, etc.) cancel their I/O when they see the cancel; the model call returns immediately with a partial response; the run loop emits any already-accumulated content and exits. Sessions, the event log, background subagents, and the attach registry all survive untouched.

Interrupt cancels but does not hold: the driver is free to start another turn on the next wake. Use InterruptAndHold for the operator "stop and wait for my next instruction" gesture (see pause.go).

func (*Agent) InterruptAndHold added in v2.9.0

func (a *Agent) InterruptAndHold(reason string) (interrupted bool, paused bool)

InterruptAndHold cancels the in-flight turn (if any) AND closes the pause gate, atomically with respect to each other: the gate is closed under the same a.mu acquisition that reads the cancel func, so a wake racing the interrupt can't slip a fresh turn in between the cancel and the hold.

Returns (interrupted, paused): interrupted is Interrupt's "there was something to cancel"; paused is the post-condition gate state, which is always true — an operator who hits interrupt while the agent happens to be idle still meant "stop", and holding is what makes that stick.

Empty reason defaults to PauseReasonOperatorInterrupt.

The interrupt audit row (#565) is armed BEFORE the cancel fires, which closes a real race: the attach handler used to call MarkInterruptPending only after AttachInterrupt returned, while drainInterruptAudit runs from the interrupted turn's own cleanup. If cleanup won, the row landed a turn late — and auto-continue, which reads that row as "deliberate kill, don't resume", could re-drive exactly the work the operator had just killed. Arming first makes the ordering deterministic.

func (*Agent) MarkInterruptPending added in v2.8.0

func (a *Agent) MarkInterruptPending()

MarkInterruptPending is the agent side of attach.InterruptSelfAuditor. The attach /interrupt handler calls it (via the adapter) after an operator cancel actually fired, instead of appending the audit row out-of-band. The row is then written from the post-turn cleanup (see drainInterruptAudit) once the interrupted turn has fully unwound.

Deferring the write this way is the whole fix for #565: the handler's out-of-band Get-then-AppendEvent bumped the live session row's last_update_time while the runner was still flushing the interrupted turn, tripping ADK's optimistic-concurrency check so the operator's clean cancel surfaced as an opaque "stale session error". Riding the turn loop moves the write to the one window with no live runner handle.

func (*Agent) MeterProvider added in v2.9.0

func (a *Agent) MeterProvider() metric.MeterProvider

MeterProvider returns the resolved OTel MeterProvider the agent's metric instruments were built from (the WithMeterProvider value, or the process-global resolved at New). Subagent construction sites use it to propagate an embedder's non-global provider down the tree — the background spawn path threads it into the spawned agent so subagent turns/tools land in the same provider as the parent rather than silently falling back to the global. Nil only on a hand-constructed Agent (tests); callers thread it conditionally.

func (*Agent) Mode added in v2.8.0

func (a *Agent) Mode() Mode

Mode reports the layer-3 overlay mode the agent was built with (#459). The autonomous driver consults this to warn when an interactive-mode agent is driven autonomously. Note a WithInstruction full-replace skips the overlay entirely; Mode still reports whatever WithMode set (default ModeInteractive) — the warning is advisory, and full-replace consumers know what they're doing.

func (*Agent) Model added in v2.8.0

func (a *Agent) Model() adkmodel.LLM

Model returns the LLM the agent was constructed with (#510). Exposes the value New received so drivers that accept a pre-built Agent — runner.Run in particular — can derive the streaming model from the agent instead of demanding it be passed alongside (the agent/model mismatch hazard #492 flagged). Nil-safe.

func (*Agent) ModelName

func (a *Agent) ModelName() string

ModelName returns the name of the LLM the agent was constructed with (sourced from model.Name() at New() time). Used by the attach-mode /status endpoint so the TUI usage panel can label the in/out/cost figures with the model in use.

func (*Agent) Pause added in v2.9.0

func (a *Agent) Pause(reason string) bool

Pause closes the pause gate: no NEW turn starts until Resume (or a non-auto-continue Inject, which resumes implicitly — see InjectAs). A turn already in flight is deliberately left alone and runs to completion; there is no safe suspend point inside a model call, and reporting "paused" while tokens keep burning would be a lie. Use InterruptAndHold when the in-flight turn should die too.

Idempotent: reports whether this call actually closed the gate. Empty reason defaults to PauseReasonOperatorPause.

See docs/operator-interrupt-design.md for the full state machine.

func (*Agent) PauseState added in v2.9.0

func (a *Agent) PauseState() PauseState

PauseState snapshots the gate for operator surfaces.

func (*Agent) Paused added in v2.9.0

func (a *Agent) Paused() bool

Paused reports whether the gate is currently closed. Cheap enough for a status poll.

func (*Agent) PendingInboxCount

func (a *Agent) PendingInboxCount() int

PendingInboxCount peeks at the inbox without draining it. Useful for UI surfaces (TUI queue panel) that need to render the current queue length without claiming the messages.

Returns 0 for nil agent / inbox.

func (*Agent) QueueAsContext added in v2.9.0

func (a *Agent) QueueAsContext(ctx context.Context, message string, caller auth.Caller) error

QueueAsContext files a message for the agent to read on its next turn WITHOUT causing that turn (#698). It is InjectAsContext minus the one thing that makes an inject preemptive: it does not fire the wake signal, so it cannot cut a sleep short. Everything else is identical — same queue, same prompt_id, same `inbox`/queued event, same caller and trace-context handling, same drain path, same last-caller-wins originator rule.

It used to differ on a second axis too: an inject opened a pause gate an operator had closed and this did not. Since #878 neither does, so against a PARKED agent the two are equivalent — both leave the message waiting for the operator's resume. The distinction that remains is about a SLEEPING agent, which is the one #698 was for.

This is the "file this away" primitive behind POST /inject with {"wake": false}. Its use case is a machine producer — an alert watcher, a monitoring hook — that has corroborating context worth having but no claim on the agent's attention right now. Before it, every such message drove its own turn: two watcher signals two minutes apart meant two wakes, the second landing while the agent was still working the first. Queued, they drain together as one block on whatever turn happens next.

It makes NO promptness guarantee, and callers must treat that literally. The message is drained by the next turn, but nothing here causes a next turn: an autonomous loop reaches one on its own sleep timer, an operator-driven session reaches one when the operator says something, and a session that is parked reaches one when it is resumed. If the message needs to be acted on, inject it normally.

Zero-value caller and the ctx rules are exactly InjectAsContext's; see there.

func (*Agent) QueueAsContextWithID added in v2.9.0

func (a *Agent) QueueAsContextWithID(ctx context.Context, message string, caller auth.Caller) (string, error)

QueueAsContextWithID is QueueAsContext returning the prompt_id it assigned — the deferred sibling of InjectAsContextWithID, and see there for why the id is worth having and why it is not a turn identity.

Both deliveries report an id because the client-side problem is the same on either: a deferred message still drains into a turn, still produces an `inbox`/queued frame, and still leaves a gateway with nothing to key on. Reporting the id on only the waking path would make the response shape depend on a flag in the request, which is the kind of conditional field clients read wrong.

func (*Agent) RecordGuardrailReset added in v2.9.0

func (a *Agent) RecordGuardrailReset(reset []string, budgetUSD float64, caller string)

RecordGuardrailReset persists an operator's reset (#643) and is the audit trail #331 asked for — one row, not two, so there is nothing to keep in agreement. reset names the guardrails whose flag was cleared, budgetUSD the runway added, caller the authenticated identity (empty when the reset came from an unauthenticated in-process surface).

Call it AFTER the state mutations, once per operator action. A reset that cleared nothing and added nothing writes nothing: a defensive reset against a healthy session is not an event.

func (*Agent) RequestWake

func (a *Agent) RequestWake()

RequestWake fires the agent's wake signal AND publishes a `wake` event to connected operators. Callers, as the tree actually stands:

  • The attach-mode `POST /sessions/<id>/wake` endpoint, when an operator outside the process wants an immediate rescan. This is the only caller in cmd/ or pkg/ that a shipped binary reaches.
  • BackgroundAgentManager.pushAlert, on every alert a subagent reports. Background alerts are otherwise PULLED at the top of a parent turn, so a child that finishes after the parent's last turn reports into a queue nothing is scheduled to read; the wake is what makes "result will be pushed" true (#780).
  • autonomous.Handle.RequestWake, the host-facing door, for wakes the host knows about and the runtime doesn't. Nothing in-tree wires it to anything now that the manager wakes for itself; dev/uat/scheduled-monitor is the worked example of a host doing the same thing by hand.
  • ResumeWith(mode, "", caller) with no message — reachable from a library caller only. `POST /resume` never takes it: attachadapter frames a message for both steer and continue, and a non-empty message short-circuits ResumeWith before the wake.

Operator input via Agent.Inject fires the signal too, but through the unexported path — it deliberately does NOT publish the event. See injectAs.

The event is published here rather than from the attach handler for the same reason emitPause is: an attached operator has to see every wake, and the wake paths above are mostly NOT HTTP ones — the host wiring and the library call never touch a handler, and putting the emit in the handler would make them invisible to exactly the remote operator who can't see the process (#802). Publishing is also the only way an operator surface OUTSIDE this process can learn about a wake: SubscribeWake hands out in-process channels, and an attached TUI is at the other end of an SSE stream. (When #802 shipped, the event was the only way any operator surface could learn about a wake, in-process ones included, because the wake channel was single-consumer and a second subscriber would have stolen wakes from the autonomous scheduler it exists to interrupt. #813 replaced that channel with the fan-out below; the reason to emit here did not change.)

emit is a no-op with no operator transport wired, and the attach broadcaster's fan-out is non-blocking per subscriber, so this stays safe to call from a hot path. The wake fan-out is non-blocking per subscriber too and takes no lock at all. It is also safe to call while the loop is running: nothing here takes the agent's state lock.

No-op when the agent has no wake signal (defensive: hand-constructed Agent structs used in tests don't necessarily wire one up) — but the event still publishes, because "something asked for a wake" is true regardless of whether a scheduler was listening.

func (*Agent) ResetCostCeiling

func (a *Agent) ResetCostCeiling()

ResetCostCeiling clears any tripped cost-ceiling flag, allowing the agent to accept new turns again. Typically wired to an operator slash command after the operator has reviewed why the ceiling tripped. Safe to call even if no ceiling is configured or no flag was set — no-op in that case.

A bare reset is enough for a per-TURN trip: the next turn starts from a fresh baseline. It is NOT enough for a per-SESSION trip — the accumulator is already at or past the ceiling, so the very next turn re-trips. Pair it with AddSessionCostBudget (see WouldRetripCostCeiling) to hand the session real runway.

func (*Agent) ResetWatchdog added in v2.9.0

func (a *Agent) ResetWatchdog()

ResetWatchdog clears a tripped enforce-mode watchdog, letting the agent accept new turns again. Typically wired to an operator slash command after the operator has reviewed why the watchdog tripped. Also resets the underlying watchdog's signal state so the next run of identical calls has to build back up to the threshold. Safe to call when nothing tripped — no-op in that case.

Deliberately does NOT drop queued feedback (#159). A reset resumes a model whose context still ends in the loop it was halted for; the queued observation is the only thing that stops the first post-reset turn from re-issuing the same call. Clearing it here would make the reset undo the correction along with the halt.

func (*Agent) RestoreGuardrails added in v2.9.0

func (a *Agent) RestoreGuardrails(ctx context.Context) error

RestoreGuardrails folds this session's durable guardrail rows and applies the result: a halt that survived a restart is re-armed, and budget an operator granted before the restart is re-applied to the per-session ceiling.

Idempotent and cheap to over-call — the fold applies at most once per agent, and Run calls it before its first pre-flight, so most callers never need it. Exposed for embedders that want the state restored (and any error surfaced) before accepting traffic rather than on the first turn.

The latch is set on SUCCESS only, so a read that fails is retried on the next call rather than leaving the agent permanently unrestored. A sync.Once here would mean one transient database error at the wrong moment disarms the backstop for the whole life of the process — the exact failure this durability work exists to remove.

Returns an error only when the session read fails. A session with no guardrail history restores nothing and reports success.

func (*Agent) Resume added in v2.9.0

func (a *Agent) Resume() bool

Resume opens the pause gate so the next turn can start. Idempotent: reports whether this call actually opened it (false when the agent wasn't paused, so a double-click from two operator surfaces isn't an error).

Resume does NOT itself drive a turn. Callers that want the agent to pick work back up pair it with an Inject (steer text or a continue note) and RequestWake — see the resume dispositions in docs/operator-interrupt-design.md.

func (*Agent) ResumeWith added in v2.9.0

func (a *Agent) ResumeWith(mode, message string, caller auth.Caller) (bool, error)

ResumeWith is the operator's full resume disposition in one call: queue the message (if any), open the gate, and wake the loop — in that order.

The order is the point. Injecting first means the steer is already on the queue when the gate opens, so the turn the resume releases picks it up; opening the gate first leaves a window where a wake loop blocked in awaitResume starts an un-steered turn and the operator's instruction lands a turn late, against work they'd just redirected.

message is the already-framed text (see FormatInterruptSteer / FormatInterruptContinue) — empty for an abandon, which opens the gate and wakes nothing. mode is carried on the `pause` SSE event.

Returns whether the gate was actually open()ed by this call; false (with no error) when the agent wasn't paused, which is the idempotent case, not a failure. A queued message is still queued and woken in that case: an operator whose resume raced someone else's still gets their instruction delivered.

ResumeWith takes no context, so a resume-steer message carries no trace context and contributes no link to the turn it shapes (unlike InjectAsContext). Deliberate for now: changing this exported signature would break every caller, and the resume path is not the one the cross-process watcher→daemon story runs through.

func (*Agent) ResumeWithMode added in v2.9.0

func (a *Agent) ResumeWithMode(mode string) bool

ResumeWithMode is Resume carrying the operator's disposition ("steer" / "continue" / "abandon") for the `pause` SSE event, so a second client watching the same session can render what the operator chose rather than just that something changed. The mode is purely observational — the injecting is the caller's job either way.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, prompt string) iter.Seq2[*session.Event, error]

Run executes one turn of the agent against prompt and returns the event iterator straight from ADK's runner. Callers are expected to range over the returned iter.Seq2 and consume events as they arrive — partial text chunks, tool calls, and the final TurnComplete event.

Multi-turn use: call Run() repeatedly on the same Agent. The configured session ID is reused across calls, so the ADK accumulates conversation history automatically.

When a BackgroundAgentManager is wired via WithBackgroundManager, any alerts background subagents have emitted since the last turn are drained (non-blocking) and prepended to the prompt so the parent's model sees them before deciding what to do next.

Inbox messages queued via Agent.Inject from external callers (harness, orchestrator, HTTP handler) are also drained and prepended, sibling to the alerts block. Ordering: alerts go first (internal state changes); inbox goes second (external input, closer to the prompt logically); then the original prompt.

func (*Agent) RunSubtask

func (a *Agent) RunSubtask(ctx context.Context, spec SubtaskSpec) (SubtaskResult, error)

RunSubtask runs a synchronous, fresh-context, single-purpose LLM call against the parent's session.Service (with a distinct branch so events don't leak into the parent's next Run request). Returns the model's digested answer.

Properties:

  • Caller blocks until the subtask returns.
  • The subtask runs in its own ADK session (deriveSubagentSessionID produces a parent-prefixed ID; the audit log can correlate).
  • The subtask sees NO parent history; its model gets only the SystemPrompt + UserMessage from the spec.
  • The subtask's tool set is the spec's Tools — nothing inherited from the parent.
  • Cost rolls up to the parent's usage.Tracker (when one is wired via WithUsageTracker) so /stats reflects everything.
  • On budget exhaustion: returns SubtaskResult{Truncated: true} with whatever partial Digest accumulated. NOT a Go error.
  • On model failure or other unrecoverable error: returns the wrapped error; caller can errors.Is on transport vs API vs spec-validation failure.

Used by the agentic tool wrappers in core-agent/tools ( AgenticReadFile, AgenticFetchURL, AgenticGrep, AgenticResearch); also directly callable by host code that wants to spawn a one-shot research subagent without going through a tool.

func (*Agent) RunWithContents

func (a *Agent) RunWithContents(ctx context.Context, contents []*genai.Content) iter.Seq2[*session.Event, error]

RunWithContents drives one agent turn from a pre-built conversation history (genai.Contents) instead of a single prompt string. The trailing message is treated as the new user input; everything before it is pre-populated into a fresh session as history events.

Each call uses a fresh sessionID so prior calls don't accumulate state — the caller-supplied history is authoritative. Use this when integrating with a runtime (the AX adapter is the motivating example) that supplies the full conversation history per turn rather than relying on a session-managed prompt.

The last content's Role must be genai.RoleUser; non-user trailing messages return an error. Empty contents return an error.

func (*Agent) SessionCostUSD added in v2.9.0

func (a *Agent) SessionCostUSD() float64

SessionCostUSD reports the session's cumulative spend as the ceiling enforcement sees it — the same accumulator a per-session trip is measured against. 0 when no usage tracker is wired.

func (*Agent) SessionID

func (a *Agent) SessionID() string

SessionID returns the session identifier the agent was constructed with. Combined with AppName + UserID this is the key the event log uses to scope ForSession queries.

func (*Agent) SessionService

func (a *Agent) SessionService() session.Service

SessionService returns the session.Service backing this agent. When no WithSessionService option was passed at construction this is the default in-memory service. Useful for callers that want to query session state directly (e.g. listing prior events) without keeping their own reference to the Service they passed in.

func (*Agent) SessionTitle added in v2.9.0

func (a *Agent) SessionTitle() string

SessionTitle returns the short operator-facing label for this session, or "" when none has been set or generated yet.

Safe to call from any goroutine — the attach layer reads it from an HTTP handler while the turn goroutine may be writing it.

func (*Agent) SetAttachEmitter deprecated

func (a *Agent) SetAttachEmitter(f func(eventType string, payload any))

SetAttachEmitter is the pre-#506 name of SetOperatorEventEmitter — the typed operator-event seam is transport-neutral; only its first consumer was attach.

Deprecated: use SetOperatorEventEmitter.

func (*Agent) SetOperatorEventEmitter added in v2.8.0

func (a *Agent) SetOperatorEventEmitter(f func(eventType string, payload any))

SetOperatorEventEmitter installs (or clears, when f is nil) the callback the agent uses to push typed operator events — status-update, usage-update, turn-complete, turn-error, inbox — to whatever transport is listening. The attach SSE broadcaster is today's only consumer (it wires its Emit on first subscriber and clears it when the last disconnects), but the seam itself is transport-neutral (#506; formerly SetAttachEmitter, which baked one transport's name into the frozen core surface).

When a tracker is wired via WithUsageTracker, this also installs (or clears) a tracker.SetOnAppend callback that emits a usage-update event with cumulative + per-model totals after every Append. That's what carries the "running cost" the spec describes for the usage-update event type — the turn-complete event reports 0 for cost because the agent itself has no pricing reference (pricing lives in the harness).

Optional. When no callback is installed, all agent-side emit calls are no-ops — events are dropped to the floor since no consumer can see them. This matches the protocol's design intent: typed events are operator-visible signals, not audit log entries; if there's no operator, there's nothing to signal.

Safe to call concurrently with the agent's own emit path; the internal mutex serializes the swap and any in-flight emit reads.

func (*Agent) SetSessionTitle added in v2.9.0

func (a *Agent) SetSessionTitle(title string)

SetSessionTitle overrides the session's title and suppresses any generation that has not already started. Empty (or whitespace-only) input clears the title and re-arms generation, which is what an operator clearing a bad name would expect to happen.

This is the manual-rename path. An inferred title with no override is a worse deal than no title at all: the operator can see it is wrong and can do nothing about it.

func (*Agent) Streaming added in v2.8.0

func (a *Agent) Streaming() adkagent.StreamingMode

Streaming returns the ADK streaming mode the agent was constructed with. Part of the read-only accessor seam the split-out subagent packages (pkg/agent/background) use to build child agents that inherit the parent's streaming behavior without reaching the unexported field directly.

func (*Agent) SubagentNames added in v2.9.0

func (a *Agent) SubagentNames() []string

SubagentNames returns the resolved tool names of the declarative subagents registered via WithSubagents (empty when none). These are synchronous parent-callable subagent tools; operator surfaces use the set to classify their tool-source as "subagent" (#627), distinct from ordinary built-ins. The result is a copy — callers may mutate it.

func (*Agent) SubscribeWake added in v2.9.0

func (a *Agent) SubscribeWake() (<-chan struct{}, func())

SubscribeWake registers an independent wake subscription and returns its receive end plus an unsubscribe func. Every subscriber sees every wake: RequestWake and Inject fan out to all of them with one non-blocking send apiece, so a subscriber that isn't reading drops only its own redundant wakes.

Same per-channel semantics as WakeRequested — buffer 1, drop on full, "something happened, re-check state" rather than an event count. A wake fired after the subscription exists but before the caller starts reading is latched, not lost; one fired before it exists is not, so subscribe before the agent goes live if that window matters. (WakeRequested's channel has no such window: it is allocated with the agent.)

This is what an operator surface should use. The local `--tui` adapter is the in-tree caller: it holds one subscription for the lifetime of the TUI so an operator's toast and the driver's wake no longer consume each other (#813).

Lifecycle: the returned func is idempotent and safe to call from any goroutine. It deregisters the subscription; it does NOT close the channel, because closing races a concurrent fire and because a closed channel reads downstream as a permanently-ended subscription rather than a quiet one. An unsubscribed channel goes silent, which is what a select wants. Call it — a subscription nobody releases is retained for the agent's lifetime, which is one leaked channel per TUI attach or per host session that forgets.

Nil-safe on both a nil *Agent and a hand-constructed Agent with no wake signal wired: the channel comes back nil (a nil channel in a select blocks forever, the correct "no wake source" behavior) and the unsubscribe is a non-nil no-op, so callers can `defer` it unconditionally.

func (*Agent) Tools

func (a *Agent) Tools() []tool.Tool

Tools returns the resolved tool list the agent was constructed with — including any subagent tools materialized by WithSubagents. Useful for diagnostics ("does my parent know about the research subagent?") without introspecting ADK internals.

func (*Agent) Tracker added in v2.8.0

func (a *Agent) Tracker() *usage.Tracker

Tracker returns the usage.Tracker the agent was constructed with via WithTracker, or nil when none was wired. Part of the read-only accessor seam pkg/agent/background uses to roll background subagent turns into the parent's usage totals.

func (*Agent) TurnInFlight added in v2.9.0

func (a *Agent) TurnInFlight() bool

TurnInFlight reports whether a Run turn is currently executing on this agent — the same signal Interrupt() acts on, exported so callers that must not act on a session mid-turn can ask instead of guessing.

The guarantee it carries is narrow but exact: registration happens in Run before runner.Run is handed the turn's message, and the clear happens in the post-drain cleanup, after the last event has been yielded (and therefore committed). Every event the RUNNER writes for the turn — the user message that opens it included — is thus written inside the interval where this reports true. That is what lets pkg/compose's auto-continue tell "the tail is an unanswered user message because the turn was interrupted" from "…because the turn is still generating" (#796), which the tail shape alone cannot distinguish (see ClassifyInterruptedTail). Events Run commits BEFORE registration are outside the interval — today that is #537's tail repair, which writes synthesized responses for a turn that was already interrupted, so a reader that sees them and no in-flight turn draws the same conclusion it would have drawn a moment earlier.

It is per-Agent, and therefore per-session, and it knows only about turns THIS process is driving: a turn running in a peer daemon against a shared eventlog reports false here. Cross-daemon exclusion is the eventlog run lock's job (eventlog.Handle.AcquireLock), not this method's.

Returns false for a nil agent.

func (*Agent) UserID

func (a *Agent) UserID() string

UserID returns the user identifier the agent was constructed with.

func (*Agent) WakeRequested

func (a *Agent) WakeRequested() <-chan struct{}

WakeRequested returns the agent's DEFAULT wake subscription: a channel that fires whenever RequestWake (or Inject, which fires the same signal internally) is invoked. Buffered(1) coalesced semantics: multiple wakes between drains land as one notification.

The same channel every call, for the agent's whole lifetime. Two callers rely on that and would break under a per-call subscription: core-tui re-invokes it to re-arm its listener after every wake, and the autonomous driver re-invokes it on every turn that schedules a successor, to attach it to the context it passes to Scheduler.BeforeNextTurn, where SleepScheduler selects on it alongside its sleep timer and ctx.Done.

Which is also the one caveat: because it is the same channel, two concurrent readers of THIS method still race each other for each value, exactly as they did before #813. That is fine for its callers as the tree stands, because they are the agent's driver — pkg/runner's REPL loop, pkg/runner.WakeLoop, and the autonomous scheduler — and an agent has exactly one driver at a time. Anything that merely wants to OBSERVE wakes alongside a driver must call SubscribeWake instead; that is the bug #813 fixed in the local TUI adapter, which used to hand this channel to core-tui and take turns with the scheduler for each wake.

func (*Agent) WatchdogMode added in v2.9.0

func (a *Agent) WatchdogMode() string

WatchdogMode reports the agent's configured watchdog posture as one of the config.Watchdog* strings: "off" (no watchdog wired), "warn" (observe and alert), "feedback" (observe, alert, and tell the model on its next turn), or "enforce" (all of that, and halt).

Exposed because "is the backstop actually on?" is the question #642 exists to answer, and the answer must be checkable from outside the package — by the startup summary, by operator surfaces, and by the wiring tests that keep a future refactor from quietly dropping the WithWatchdogEnforce option on a daemon's session-created agents.

func (*Agent) WatchdogTripped added in v2.9.0

func (a *Agent) WatchdogTripped() (bool, string)

WatchdogTripped reports whether the agent is currently blocking new turns because the enforce-mode watchdog fired. Exposed for /stats and similar surfaces so "why is the agent refusing my prompts?" has an obvious answer. Returns (true, reason) when blocked; (false, "") otherwise.

func (*Agent) WouldRetripCostCeiling added in v2.9.0

func (a *Agent) WouldRetripCostCeiling() (retrip bool, spent, ceiling float64)

WouldRetripCostCeiling reports whether clearing the trip flag right now would be immediately undone — the session's accumulated spend is already at or past the per-session ceiling, so the next turn's enforcement pass trips again before the operator sees any progress. Returns the spend and the ceiling so the caller can say so precisely.

This is the check that keeps the reset affordance honest: offering a button that provably does nothing is the same state-a-property-you-don't-enforce pattern the reset exists to fix.

type AgentSource added in v2.8.0

type AgentSource interface {
	Agents() []*Agent
}

AgentSource enumerates live agents for RegisterMetrics to sample on each export interval. Implementations must be cheap and thread-safe; nil agents in the slice are skipped.

session.id comes from each agent's own SessionID() — unlike the usage observer, which takes the registry Entry's triple as authoritative. The two coincide in the daemon (the adapter derives Entry fields from the agent); hosts registering agents under a different Entry session id will see the two observer families disagree.

type CheckpointResult

type CheckpointResult struct {
	CheckpointEventID string
	SummaryText       string
	TaskNote          string
	Duration          time.Duration
	Skipped           bool
}

CheckpointResult reports what happened on a Checkpoint call. Same fields as CompactionResult plus TaskNote (the detail that triggered the checkpoint, surfaced so UI / telemetry can show it without re-reading the event metadata).

type Checkpointer

type Checkpointer interface {
	// ShouldCheckpoint is the heuristic gate fired post-turn when
	// the model didn't explicitly call mark_task_done. Default
	// implementation always returns false (heuristic off). Custom
	// implementations could scan the just-completed assistant
	// message for completion patterns.
	ShouldCheckpoint(ctx context.Context, a *Agent) bool

	// CheckpointInstruction returns the system instruction the
	// summarizer LLM call uses. taskNote carries the operator/
	// model-supplied completion detail (the `detail` arg of
	// mark_task_done, or the operator's /done argument).
	CheckpointInstruction(taskNote string) string
}

Checkpointer decides whether to auto-trigger task-boundary checkpoints and produces the summarizer prompt. The mark_task_done tool always triggers a checkpoint regardless of ShouldCheckpoint — the heuristic is for OPTIONAL post-turn auto-fire based on assistant-text patterns ("looks done") and is off by default. Consumers customize by implementing Checkpointer themselves.

func NewDefaultCheckpointer

func NewDefaultCheckpointer() Checkpointer

NewDefaultCheckpointer returns a DefaultCheckpointer. Pass &DefaultCheckpointer{} directly if you want to assert the type; the constructor exists for symmetry with NewDefaultCompactor.

type CompactionResult

type CompactionResult struct {
	// SummaryEventID is the ID of the event the compactor wrote to
	// the session. Empty when no compaction ran (compactor returned
	// no-op, or the call errored before writing).
	SummaryEventID string

	// SummaryText is the full text the model produced. Useful for
	// callers that want to surface the summary in the UI before the
	// next turn runs.
	SummaryText string

	// Duration is wall-clock time spent in the summarizer LLM call.
	Duration time.Duration

	// Skipped is true when the compactor decided not to compact
	// (e.g., ShouldCompact returned false from a programmatic
	// Agent.CompactIfNeeded call). When Skipped is true, the rest of
	// the fields are zero-valued.
	Skipped bool
}

CompactionResult reports what happened on a Compact call.

type Compactor

type Compactor interface {
	// ShouldCompact returns true when the agent should compact before
	// the next turn. Called from Agent.Run's post-turn hook with the
	// agent's usage tracker so the implementation can read context-
	// window state (Tracker.ContextWindowUsed / ContextWindowSize).
	// Returning false is a no-op — the next turn proceeds normally.
	ShouldCompact(ctx context.Context, a *Agent) bool

	// SummarizerInstruction returns the system instruction the
	// compactor LLM call uses. focus carries the operator's optional
	// focus hint (empty when none). The instruction tells the model
	// what kind of summary to produce; the conversation history is
	// supplied as the LLMRequest.Contents.
	SummarizerInstruction(focus string) string
}

Compactor decides when context-window compaction should fire and produces the summary prompt the agent sends to its model. Consumers plug a custom implementation via agent.WithCompactor; the package default (NewDefaultCompactor) covers the common case.

func NewDefaultCompactor

func NewDefaultCompactor() Compactor

NewDefaultCompactor returns a DefaultCompactor with the package- default fallback threshold AND the default per-tier overrides from modeltier.DefaultCompactionThresholds — so small/mid/frontier each get a tier-appropriate trigger out of the box. Pass a &DefaultCompactor{...} literal directly to customize either knob.

type ContextStats

type ContextStats struct {
	// Compaction* report on Mechanism A boundary events.
	CompactionCount     int
	LastCompactionFocus string    // CompactionFocusKey from the last compaction event (empty when none)
	LastCompactionTime  time.Time // zero when none

	// Checkpoint* report on Mechanism C boundary events.
	CheckpointCount    int
	LastCheckpointNote string    // CheckpointNoteKey from the last checkpoint event (empty when none)
	LastCheckpointTime time.Time // zero when none

	// TotalSummaryChars is the aggregate character count of all
	// boundary summary text (compaction + checkpoint) written
	// this session. Proxy for "how much history has been
	// compressed" — useful as a sanity check that compaction is
	// actually doing something.
	TotalSummaryChars int

	// Subtask* report on Mechanism B usage. Count + tokens + cost
	// are accumulated in recordSubtaskUsage; usage.Tracker totals
	// (/stats) include this same cost in their grand total.
	SubtaskCount        int
	SubtaskInputTokens  int
	SubtaskOutputTokens int
	SubtaskCostUSD      float64

	// ModelBreakdown surfaces /stats-style per-model totals when
	// multiple models were used in the session (typically: parent
	// on a frontier model, subtasks on a cheap flash/haiku-tier
	// model via --agentic-small-model). Pulled from
	// usage.Tracker.TotalsByModel; empty when no tracker is wired
	// or only one model has been used (in which case the
	// breakdown wouldn't tell the operator anything new beyond
	// /stats' grand total).
	ModelBreakdown map[string]usage.Totals

	// DigestSavings surfaces the MCP digest wrap's cumulative
	// effect on the parent's context (#223). Structural and agentic
	// paths are broken out because their cost math differs — the
	// renderer labels the block "savings vs. no-digest baseline"
	// since these are hypothetical (what it would have cost without
	// the wrap layer), not real spend reductions. Zero-value when
	// the wrap layer is off or nothing has fired yet.
	DigestSavings usage.DigestSavingsTotals
}

ContextStats is a snapshot view of the three context-management mechanisms wired on the Agent. All fields are zero-value-safe; the consumer renders "no compactions yet" / "no subtasks yet" based on the counts.

Boundary fields (Compaction*, Checkpoint*, TotalSummaryChars) are derived from the session event log on each call. Subtask fields come from in-memory counters bumped in RunSubtask.

type CostCeiling

type CostCeiling struct {
	// MaxTurnUSD is the cap on a single conversation turn's spend
	// (cumulative cost of every model call between one operator
	// inject and the next agent-done state). Tripped → next Run
	// refuses with an ErrCostCeilingExceeded error.
	MaxTurnUSD float64

	// MaxSessionUSD is the cap on the session's cumulative spend
	// across all turns (parent + subtask).
	MaxSessionUSD float64
}

CostCeiling configures the per-turn / per-session spend caps the post-turn hook enforces. Zero or negative values disable that specific ceiling — both fields default to disabled when constructed via the zero value.

type DefaultCheckpointer

type DefaultCheckpointer struct{}

DefaultCheckpointer is the package-default Checkpointer. Heuristic is off (ShouldCheckpoint always false) — mark_task_done + /done are the trigger paths. Prompt mirrors DefaultCompactor's five-section handover plus a "Completion record" preamble that names the just-finished task as the focal point of the summary.

func (*DefaultCheckpointer) CheckpointInstruction

func (c *DefaultCheckpointer) CheckpointInstruction(taskNote string) string

CheckpointInstruction returns the five-section handover prompt with a "Completion record" preamble that names the just- finished task. The model's summary will frame the conversation from "this task is now done" angle rather than the "we're still mid-task" angle DefaultCompactor produces.

func (*DefaultCheckpointer) ShouldCheckpoint

func (c *DefaultCheckpointer) ShouldCheckpoint(_ context.Context, _ *Agent) bool

ShouldCheckpoint returns false. Heuristic-based auto-checkpoint is intentionally off by default — false positives (declaring a task done when the operator is mid-thought) are costly. The mark_task_done tool gives the model an explicit signal it can invoke when it's confident; /done gives the operator the same.

type DefaultCompactor

type DefaultCompactor struct {
	// Threshold is the fallback context-window utilization at which
	// compaction fires when no per-tier override applies (unknown
	// model, or the tier isn't in ThresholdByTier). 0.85 means
	// "compact once we've used 85% of the model's context window."
	// Zero is treated as "use the package default."
	Threshold float64

	// ThresholdByTier overrides Threshold per modeltier classification.
	// Keys are pkg/modeltier tier labels ("frontier", "mid", "small").
	// When the current model classifies to a tier present here, that
	// tier's threshold wins. Empty map (or zero value at a key) falls
	// back to Threshold. Use this to keep frontier sessions at 0.85
	// while compacting small-tier sessions much earlier.
	ThresholdByTier map[string]float64

	// TierClassifier is the function used to look up the current
	// model's tier. Defaults to modeltier.Classify. Override in tests
	// to inject deterministic tier resolutions without depending on
	// the modeltier table's current state.
	TierClassifier func(modelID string) string
}

DefaultCompactor is the package-default Compactor. Triggers on context-window utilization ≥ a per-model threshold (default 0.85 for frontier, 0.65 for mid, 0.35 for small; see pkg/modeltier.DefaultCompactionThresholds) and produces a five-section "teammate handover" summary (current state, files & changes, technical context, strategy & approach, exact next steps) per docs/context-management-design.md §Mechanism A.

Consumers needing a different prompt or trigger logic implement Compactor themselves; this type is a sensible default, not a required base class.

func (*DefaultCompactor) ShouldCompact

func (c *DefaultCompactor) ShouldCompact(_ context.Context, a *Agent) bool

ShouldCompact returns true when the agent's usage tracker reports context-window utilization at or above the resolved threshold for the current model's tier. Returns false when the tracker doesn't yet know the window size (no turn has landed, or the model isn't in usage.ContextWindowSizeFor's table) so a session with an unknown model never triggers premature compaction.

func (*DefaultCompactor) SummarizerInstruction

func (c *DefaultCompactor) SummarizerInstruction(focus string) string

SummarizerInstruction returns the five-section handover prompt. focus, when non-empty, appended as a "Compact focus: <text>" directive so the summarizer prioritizes that thread.

type EmptySummaryError added in v2.9.0

type EmptySummaryError struct {
	Operation string
	Detail    string
	Attempts  int
}

EmptySummaryError reports a summarizer call that completed without error and produced no text.

Detail is the provider's own explanation, in the vocabulary /btw already uses: "finish_reason=MAX_TOKENS", "error=SAFETY: ...". It is empty when the provider offered none, which is itself a signal — that is the unexplained shape, and the one this code retries.

Attempts is how many summarizer calls were made before giving up, so a reader can tell a retried-and-still-empty failure from one that was classified as terminal on the first response and deliberately not retried.

func (*EmptySummaryError) Error added in v2.9.0

func (e *EmptySummaryError) Error() string

func (*EmptySummaryError) Unwrap added in v2.9.0

func (e *EmptySummaryError) Unwrap() error

type Mode added in v2.8.0

type Mode int

Mode selects the layer-3 overlay: how the agent should carry itself given who (if anyone) is watching. Set where the agent is built — drivers never mutate a caller-supplied agent (the autonomous driver warns when it sees an interactive-mode agent; see autonomous.Run).

const (
	// ModeInteractive (the default): a user is present and can
	// redirect / answer questions.
	ModeInteractive Mode = iota
	// ModeAutonomous: nobody reads output in real time; narrate for
	// the audit record and never ask questions in output text.
	ModeAutonomous
)

type Option

type Option func(*options)

Option mutates Agent construction. Use the With* helpers below.

func WithAppName

func WithAppName(s string) Option

WithAppName overrides the AppName handed to the ADK runner. Useful when embedding so telemetry and session stores can distinguish multiple agents inside one binary.

func WithBackgroundManager

func WithBackgroundManager(mgr SubagentManager) Option

WithBackgroundManager attaches a BackgroundAgentManager to the agent. The manager's parent back-reference is set during construction so its Spawn calls can read the agent's session triple + session.Service without the consumer plumbing them twice.

Each turn of Agent.Run drains pending alerts from the manager's channel (non-blocking) and prepends them to the prompt the underlying ADK runner sees, so the parent's model is aware of what its background subagents have reported since the last turn.

Pass nil to clear (e.g. for tests that re-construct an agent).

func WithCheckpointer

func WithCheckpointer(c Checkpointer) Option

WithCheckpointer wires a Checkpointer implementation that drives task-boundary checkpoints (Mechanism C of docs/context-management-design.md). When wired, the agent automatically registers the mark_task_done built-in tool — the model can call it to signal task completion, and the post-turn hook in Run promotes that into a pending checkpoint the next Run drains by writing a richer handover record. The TUI's /done slash drives the same path manually.

Pass agent.NewDefaultCheckpointer() for the package default (heuristic off; mark_task_done + /done are the trigger paths; six-section completion-record prompt). Custom Checkpointer implementations let consumers swap in a different prompt or heuristic.

Optional. When nil, Agent.Checkpoint returns ErrNoCheckpointer and the mark_task_done tool is not registered.

To keep checkpointing but withhold the model's trigger, pair this with WithoutMarkTaskDoneTool.

func WithCompactor

func WithCompactor(c Compactor) Option

WithCompactor wires a Compactor implementation that drives context-window compaction (Mechanism A of docs/context-management-design.md). When wired, the post-turn hook in Run checks Compactor.ShouldCompact(); if true, the next Run call fires Compact() before its actual work, replacing the pre-summary history with a single summary event.

Pass agent.NewDefaultCompactor() for the package default (threshold 0.85, five-section handover prompt). Custom Compactor implementations let consumers swap in a different prompt or trigger logic.

Optional. When nil, Agent.Compact returns ErrNoCompactor and the post-turn hook is a no-op — compaction has to be wired in explicitly.

func WithCostCeiling

func WithCostCeiling(c CostCeiling) Option

WithCostCeiling wires per-turn and per-session spend caps. Pass a zero-value CostCeiling{} (or 0 in either field) to disable the corresponding bound — at least one must be > 0 for enforcement to run at all. Mirrors the usual WithX option shape.

func WithDescription

func WithDescription(s string) Option

WithDescription overrides the agent's description.

func WithEventHook

func WithEventHook(onEvent func(*session.Event), onTurnEnd func()) Option

WithEventHook wires per-event and end-of-turn observer callbacks. onEvent is called once per session.Event as events stream from Agent.Run — from inside the same iterator tap the watchdog and usage tracker already sit in, so observation is synchronous and ordered relative to the event yield. onTurnEnd is called from the post-turn cleanup that runs after wrapWithCleanup drains the iterator, alongside the watchdog / compaction / checkpoint hooks.

Either callback may be nil to disable that half of the surface. Both nil is legal and turns the option into a no-op; useful when a consumer wraps WithEventHook in a builder that always sets it.

Consumer contract: callbacks must not panic and should return quickly. A slow callback stalls the agent's event stream — synchronous by design so the hook mechanism can rely on ordering (matches pkg/hooks.Dispatcher, which spawns subprocesses with per-command timeouts to bound its own latency).

Single-slot: calling WithEventHook twice replaces the previous binding. Multi-consumer fan-out is the caller's responsibility (wrap two callbacks in one). This matches WithWatchdog's shape.

func WithEventLog

func WithEventLog(h *eventlog.Handle) Option

WithEventLog wires an eventlog.Handle into the agent — the Handle's Service becomes the agent's session.Service (so every event lands in the durable log), and the Handle is stored on the agent so callers can reach back to it for replay/watch via Agent.EventLog().

Equivalent to WithSessionService(h.Service) plus a stash of the Handle for later access; passing nil is a no-op.

func WithExtraInstruction added in v2.8.0

func WithExtraInstruction(s string) Option

WithExtraInstruction appends s as a layer-5 block (repeatable — each call appends another blank-line-separated block, in call order). The encouraged customization path: the harness contract and mode overlay stay intact underneath. Empty strings are dropped.

func WithGate

func WithGate(g *permissions.Gate) Option

the agent's metadata, so it can be surfaced over the attach-mode /tools endpoint (each tool gets a pre-flight `gate_state` field — "allowed" / "denied" / "prompted" / "denied-allow-mode" — without actually consulting the gate at request time). Optional; without it, the /tools endpoint reports an empty gate_state per tool and the TUI's auditing column is blank.

Metadata-only with one exception. The gate that mediates tool calls is normally the one wired into the tool constructors themselves, and the agent just exposes a read-only view of this one — but the subagent tools materialized by WithSubagents are constructed HERE, with no other constructor for a caller to wire a gate into, so they get this one (#758). Delegation is gated under the `spawn_agent` policy bucket regardless of which door the model used; see SubagentOptions.Gate.

func WithInstruction

func WithInstruction(s string) Option

WithInstruction replaces the assembled system instruction wholesale — the full-replace escape hatch. Layers 1–3 (core, provider quirks, mode overlay) are skipped ENTIRELY; you take on the harness contract yourself (compaction summaries, parallel-dispatch rules — tool-use degradation is on you). Layers 4–5 (WithUserInstruction / WithExtraInstruction) still append after the replacement so a custom base can compose with operator appends.

func WithMeterProvider added in v2.8.0

func WithMeterProvider(mp metric.MeterProvider) Option

WithMeterProvider overrides the OTel MeterProvider backing the agent's metric instruments (gen_ai.agent.invocation.duration and the per-tool gen_ai.tool.execution.duration wrapper). Defaults to otel.GetMeterProvider() resolved at construction time — the daemon installs its provider via telemetry.SetupMetrics before any agent is built, and when metrics are disabled the global is the noop provider, so recording costs nothing. Embedders wanting metrics must likewise install their provider (or pass one here) before calling New. Background subagents always bind to the global provider (the spawn path doesn't thread this option). Primarily useful for tests injecting a ManualReader.

func WithMetricAgentName added in v2.8.0

func WithMetricAgentName(name string) Option

WithMetricAgentName overrides the gen_ai.agent.name attribute value on the agent's metric instruments without changing the agent's actual name (WithName). Metric attribute values must stay low-cardinality: the spawn path names background subagents with MODEL-CHOSEN strings, and stamping those on a histogram would accrete one series per invented name on a long-lived daemon — it passes a fixed class-level value here instead. Defaults to the WithName value, which is operator-configured and bounded.

func WithMode added in v2.8.0

func WithMode(m Mode) Option

WithMode selects the layer-3 overlay (default ModeInteractive). Autonomous consumers — anything driving the agent with no human reading output in real time — should set ModeAutonomous where they build the agent; the in-tree spawn paths (background subagents, RunSubtask, remote spawn) do.

func WithName

func WithName(s string) Option

WithName overrides the agent's display name (visible in OTEL spans).

func WithPostConstruct

func WithPostConstruct(f func(*Agent)) Option

WithPostConstruct registers a callback invoked once the *Agent is fully built (right before New returns). Useful for late- binding patterns where the caller needs the agent pointer to wire something they registered earlier — e.g., an externally- constructed tool whose handler closure captured a *Agent placeholder. The hook fires on the happy path only; if New returns an error the hook is not called.

One hook per agent. Calling WithPostConstruct twice keeps the last one (Option-pattern overwrite semantics, same as other scalar With* options).

func WithSession

func WithSession(userID, sessionID string) Option

WithSession overrides the user/session IDs handed to the ADK runner. Reuse the same pair across Run() calls to preserve conversation history.

func WithSessionService

func WithSessionService(s session.Service) Option

WithSessionService overrides the session.Service handed to the ADK runner. The default is session.InMemoryService(), which loses all state when the process exits. Pass a durable Service (typically the one returned by eventlog.Open(...).Service when wiring the audit log + crash-resume substrate) to persist sessions across runs.

The supplied Service is also exposed via Agent.SessionService() so callers can query session state directly without keeping their own reference. Passing nil restores the default.

func WithStreaming

func WithStreaming(m adkagent.StreamingMode) Option

WithStreaming overrides the streaming mode. Default is StreamingModeSSE (required to receive Partial events).

func WithSubagentBudgets added in v2.9.0

func WithSubagentBudgets(b SubagentBudgets) Option

WithSubagentBudgets bounds one delegation to THIS agent when it is exposed as a subagent tool via WithSubagents — the value forwarded to SubagentOptions.Budgets at the parent's construction. A cap that fires returns whatever the subagent produced, labelled as a partial.

It lives on the subagent rather than on the parent for the same reason WithSubagentMaxDepth does: the cap is a property of the delegate, and a parent with several subagents needs a different one for each.

The zero value leaves every dimension unbounded, which is what this door has always been. Declarative subagents thread their config `budgets` block through this option and through the async twin's SubagentTemplate.Budgets, so one declared cap binds whichever door the subagent is reached through.

func WithSubagentMaxDepth added in v2.9.0

func WithSubagentMaxDepth(n int) Option

WithSubagentMaxDepth sets the recursion depth cap applied when THIS agent is exposed as a subagent tool via WithSubagents — the value forwarded to SubagentOptions.MaxDepth at the parent's construction. A subagent at depth >= this cap that is invoked from another subagent gets an error result rather than being allowed to recurse.

0 (the default) means "use the substrate default" (NewSubagentTool's defaultSubagentMaxDepth, currently 2). Declarative subagents thread their config `max_depth` through this option so an operator-set value is honored rather than silently dropped; a plain WithSubagents caller who never sets it keeps the default.

func WithSubagents

func WithSubagents(agents []*Agent) Option

WithSubagents registers each agent as a callable tool the parent's model can invoke by name. The subagent runs through ADK's runner using the parent's session.Service (so its events stream live into the same audit log) with session.Event.Branch set to "<parent_branch>.<subagent_name>" — ADK's contents-processor branch filter then keeps the subagent's events from leaking back into the parent's next-turn LLM request, which preserves context isolation while keeping the audit log unified.

Each subagent's tool name comes from its own WithName value, and its recursion depth cap from its own WithSubagentMaxDepth value (default 2). Use NewSubagentTool directly for the remaining per-subagent overrides (custom tool name, description, branch label).

Resolved at the end of New() so that the parent's session.Service and session triple — set by other With* options — are captured at the point the subagent tools are constructed.

func WithSystemInstructionPrefix deprecated

func WithSystemInstructionPrefix(prefix string) Option

WithSystemInstructionPrefix prepends prefix to the agent's instruction with the pre-#459 semantics: the result is a full replacement (prefix + whatever instruction was set, defaulting to the DefaultInstruction alias), so layer assembly is skipped.

Deprecated: memory belongs AFTER the core, not before it — use WithUserInstruction (layer 4). This survives through v2.8.x for consumers that composed against the old prefix arrangement and is deleted at the next breaking window together with DefaultInstruction.

func WithTitleModel added in v2.9.0

func WithTitleModel(m adkmodel.LLM) Option

WithTitleModel wires the model session titling should use. Titling is a summarization task with a six-word output, so the cheap tier is the right one: resolve it with models.ResolveSmallModel and hand the resulting LLM in.

Optional, and it is the switch that decides whether titles are generated at all. Without it the automatic path falls back to the head of the operator's own prompt and makes no LLM call — an agent must not quietly start spending parent-model calls on six-word labels because it was upgraded. A host that wants generation on a provider with no cheap tier (ResolveSmallModel's "" return) can pass the parent model here deliberately; that is a decision about someone's bill, so it belongs to the host rather than to a default in here.

func WithTools

func WithTools(ts []tool.Tool) Option

WithTools registers a set of tools the agent may call. Order is preserved but immaterial; ADK keys tools by Name.

func WithToolsets

func WithToolsets(ts []tool.Toolset) Option

WithToolsets registers groups of tools (MCP servers, skills, etc.). Each Toolset implements google.golang.org/adk/tool.Toolset and is passed to llmagent.Config.Toolsets.

func WithUsageTracker

func WithUsageTracker(t *usage.Tracker) Option

WithUsageTracker wires a shared *usage.Tracker into the agent so agent-level code (the compactor's threshold check, future per-turn rollups) can read context-window state without the consumer reaching in. The same tracker can be shared with a TUI host that already keeps one for /stats — both populate via usage.Append and read the same totals.

Optional. Nil-safe: components that read the tracker check first and degrade gracefully ("don't trigger threshold-based compaction if we don't know how full the window is").

func WithUserInstruction added in v2.8.0

func WithUserInstruction(s string) Option

WithUserInstruction installs the pkg/instruction loader's output as layer 4 (user memory: AGENTS.md and friends). Deliberately AFTER the core/overlay layers — user instructions take precedence over our defaults by ordinary instruction-following convention, and the stable-first ordering keeps the cached core prefix intact across memory edits (this inverts the deprecated WithSystemInstructionPrefix arrangement on purpose).

func WithWatchdog

func WithWatchdog(w watchdog.Watchdog, onAlert func(watchdog.Alert)) Option

WithWatchdog wires a behavioral watchdog. The agent calls w.ObserveToolCall as tool calls stream by, and w.Check from the post-turn hook. Returned alerts are passed to onAlert if non-nil; when onAlert is nil the alerts are collected and discarded each turn (useful for tests, or for hosts that read the watchdog's own state via a side channel).

Composable with everything else: pass alongside WithCompactor / WithCheckpointer / WithCostCeiling / etc. The watchdog runs in the same post-turn hook the others use.

func WithWatchdogEnforce added in v2.9.0

func WithWatchdogEnforce() Option

WithWatchdogEnforce promotes the watchdog from observe-only ("warn" mode) to a kill switch ("enforce" mode, #623). When set, a Critical alert (today: the repeated-tool-call runaway signal) trips the agent: it emits a watchdog turn-error and refuses subsequent turns until the operator calls ResetWatchdog — the same halt contract as the cost ceiling. No-op unless a watchdog is also wired via WithWatchdog. Warn- mode alerts (non-Critical) never halt, even under enforce.

func WithWatchdogFeedback added in v2.9.0

func WithWatchdogFeedback() Option

WithWatchdogFeedback routes watchdog alerts back into the model's next-turn context ("feedback" mode, #159): after a turn that tripped a signal, the next Run prepends a "watchdog" block carrying each alert's model-facing Guidance to the prompt.

The warn-mode surface it extends reaches an operator — who, on an unattended daemon, is not there, and who even at a terminal can only interrupt a turn already in flight. The party that can stop making the looping call is the model, and it never learned it was looping.

Implied by WithWatchdogEnforce, which is deliberate rather than incidental: an enforce-mode halt is cleared by an operator reset, and a reset resumes a model whose context still ends in the loop it was halted for. Without the injected observation, the very next turn re-issues the same call and re-trips — the reset would be a treadmill.

No-op unless a watchdog is also wired via WithWatchdog.

func WithoutMarkTaskDoneTool added in v2.9.0

func WithoutMarkTaskDoneTool() Option

WithoutMarkTaskDoneTool keeps the checkpointer wired but does not register the model-facing mark_task_done tool. /done, the heuristic, and Agent.Checkpoint all keep working; only the model's ability to declare its own task boundary goes away.

This is for long-lived services (#905). mark_task_done's description instructs the model to call it "generously at natural task boundaries", and its detail arg asks for a one-paragraph completion summary — framing that assumes an interactive coding session with a conversation about to shift to a new task. A daemon consuming machine signals has no such boundary, so it sees one everywhere: a live deployment produced sixteen mark_task_done calls in one session, thirteen of them rejected as no-ops, and answered an unrelated operator question with a completion report rather than an answer. A tool description outranks the persona at the point of decision.

No-op without WithCheckpointer — there is no tool to suppress.

Scoped to the parent's checkpoint tool only. Subagents never get a checkpointer, and the "mark_task_done" name they answer to is an alias for return_result wired by pkg/agent/background (#728) — a different tool that happens to share a name, and one this option must not and does not touch.

The CLI spells this --checkpoint=operator (config: checkpoint.mode).

func WithoutProviderQuirks added in v2.8.0

func WithoutProviderQuirks() Option

WithoutProviderQuirks suppresses layer 2 — for consumers that have measured their model doesn't need the workarounds, or that are running their own probes.

func WithoutSessionTitle added in v2.9.0

func WithoutSessionTitle() Option

WithoutSessionTitle disables automatic titling. The session keeps whatever SetSessionTitle puts there, so the manual-rename path still works — this turns off the LLM call, not the feature.

For deployments that never open a session picker and would rather not pay for a call they will never look at.

type PauseState added in v2.9.0

type PauseState struct {
	Paused bool
	// Since is when the gate closed (UTC). Zero when not paused.
	Since time.Time
	// Reason is one of the PauseReason* constants. Empty when not
	// paused.
	Reason string
	// Interrupted reports whether a turn was actually cancelled on the
	// way into this pause. False for a plain Pause, and false for an
	// InterruptAndHold that landed while the agent was idle — the
	// distinction the operator needs to know whether work was lost.
	Interrupted bool
}

PauseState is the projection every operator surface renders from — the TUI banner, GET /sessions/.../status, and the `pause` SSE event. Zero value means "not paused".

type SideQuestionEmptyError added in v2.9.0

type SideQuestionEmptyError struct {
	Detail string
}

SideQuestionEmptyError carries whatever the provider said about WHY the answer was empty. Detail is free-form and may be empty (some providers just return nothing); when set it looks like "finish_reason=SAFETY" or "error=RESOURCE_EXHAUSTED: ...", which is the difference between "retry" and "rephrase" for the operator.

func (*SideQuestionEmptyError) Error added in v2.9.0

func (e *SideQuestionEmptyError) Error() string

func (*SideQuestionEmptyError) Unwrap added in v2.9.0

func (e *SideQuestionEmptyError) Unwrap() error

type SubagentBudgets added in v2.9.0

type SubagentBudgets struct {
	// MaxTurns caps the subagent's OWN model turns — its tool/model
	// loop, not the parent's turn count.
	MaxTurns int
	// MaxCostUSD caps the dollars one delegation may spend, priced per
	// turn exactly as the session ledger prices it. Enforcement needs a
	// cost signal on this door, which is why it could not exist before
	// the turns were billed at all (#713).
	//
	// A model the pricing catalog does not know prices at zero, so this
	// dimension never binds for it — the same property the asynchronous
	// door's MaxCost has. MaxTurns is the dimension that holds
	// regardless of pricing data, so a cost cap is worth pairing with
	// one.
	MaxCostUSD float64
	// MaxWallclock caps elapsed time from the start of the delegation.
	MaxWallclock time.Duration
}

SubagentBudgets caps one synchronous delegation along the same three dimensions the asynchronous door bounds a spawned run with (background.Budgets). Zero means unbounded on that dimension.

It is declared here rather than imported because pkg/agent/background imports pkg/agent, not the other way round; cmd/core-agent fills both from one config.SubagentBudgets so an operator's declared cap binds whichever door the subagent is reached through.

A cap that fires does not fail the tool call. Whatever the subagent produced is returned, labelled as a partial and naming the cap that stopped it — discarding a partial makes the parent pay twice for work it already bought (#691), and the parent holds the goal, so it is the one that can re-ask with specifics (#730).

type SubagentManager added in v2.8.0

type SubagentManager interface {
	// AttachParent sets the manager's parent back-reference so its
	// spawn calls can read the agent's session triple + session.Service
	// without the consumer plumbing them twice. Called once during
	// Agent construction.
	AttachParent(*Agent)

	// PrependPendingAlerts drains any pending background alerts
	// (non-blocking) and prepends them to the prompt the underlying ADK
	// runner sees, returning the augmented prompt. Called each turn of
	// Agent.Run.
	PrependPendingAlerts(prompt string) string

	// ListSubagents returns attach-facing metadata for the manager's
	// live subagents. Backs attachadapter.AttachAgents
	// (attach.AgentsProvider).
	ListSubagents() []attach.AgentInfo

	// ListSubagentCatalog returns the CONFIGURED subagent roster —
	// declarative templates + predefined catalog specs — as opposed to
	// ListSubagents' live/spawned instances. Backs
	// attachadapter.AttachSubagentCatalog (attach.SubagentCatalogProvider,
	// #627).
	ListSubagentCatalog() []attach.SubagentCatalogInfo

	// SpawnSubagent spawns a subagent from an attach spec. Backs
	// attachadapter.AttachSpawnSubagent (attach.SubagentSpawner).
	SpawnSubagent(ctx context.Context, spec attach.SubagentSpec) (attach.SubagentSpawnResponse, error)

	// StopSubagent stops one subagent by name, reporting what the
	// attempt actually did: whether the name is registered at all,
	// whether this call was the thing that halted it, and the status
	// it now carries. Backs attachadapter.AttachStopAgentOutcome
	// (attach.AgentStopReporter) — the operator route for a runaway
	// subagent, which interrupting the PARENT can't reach.
	StopSubagent(name string) (attach.StopAgentOutcome, error)
}

SubagentManager is the narrow seam the core Agent uses to talk to a subagent/background manager without importing the background package (which imports agent, and would otherwise form an import cycle). The concrete implementation lives in pkg/agent/background; wire one via WithBackgroundManager.

Every method is expressed in core/attach/primitive types only — no background-package types leak across this boundary. Callers that need the richer *background.Manager surface (repl, coretui) recover it with background.ManagerOf(agent).

type SubagentOptions

type SubagentOptions struct {
	// Inner is the *agent.Agent to expose as a tool the parent's
	// model can call. The tool's function name comes from
	// Inner.AgentName() (set via agent.WithName), unless overridden
	// via Name. The tool's description comes from Inner's
	// llmagent.Description, unless overridden via Description.
	Inner *Agent

	// Name overrides the function name shown to the parent's model.
	// Empty falls back to Inner.AgentName().
	Name string

	// Description overrides the function description shown to the
	// parent's model. Empty falls back to Inner's Description (or a
	// generic fallback when that's also empty).
	Description string

	// MaxDepth caps recursion depth. A subagent at depth >= MaxDepth
	// that is invoked from another subagent gets an error result
	// rather than being allowed to recurse. Default 2; pass a
	// larger value if your agent topology genuinely needs deeper
	// nesting.
	MaxDepth int

	// Branch overrides the branch label appended to the parent's
	// branch on the subagent's events. Defaults to the tool name
	// (which is Inner.AgentName() unless Name overrides it). The
	// resulting branch is "<parent_branch>.<this>".
	Branch string

	// ParentService, when non-nil, overrides the session.Service
	// the subagent's runner uses. The agent.WithSubagents
	// convenience option fills this in automatically with the
	// parent agent's service so subagent events land in the
	// parent's audit log without any consumer plumbing.
	//
	// When nil, NewSubagentTool falls back to Inner.SessionService()
	// — which is fine for callers who construct subagents
	// pre-wired against the same Handle.
	ParentService session.Service

	// ParentAppName, ParentUserID, ParentSessionID identify the
	// parent's session triple. When set, the subagent runs through
	// the parent's session row (with branch isolation) so cross-
	// session audit queries find both. Empty values fall back to
	// Inner's own AppName/UserID/SessionID. Set automatically by
	// agent.WithSubagents.
	ParentAppName   string
	ParentUserID    string
	ParentSessionID string

	// Gate, when non-nil, is consulted before the subagent runs — so
	// plan-first, the allow/deny policy and the ask prompt apply to
	// the act of delegating and not only to what the delegate does
	// once it is running (#758).
	//
	// The lookup is deliberately made under the SAME policy bucket the
	// asynchronous door uses: a declarative subagent is reachable both
	// as this tool and as `spawn_agent {agent: "<name>"}`, and an
	// operator who wrote `deny: ["spawn_agent:cluster"]` meant that
	// `cluster` does not run — not that it does not run through one of
	// the two doors. So the rule is matched as
	// `spawn_agent:<subagent-name>` whichever door the model picked.
	//
	// agent.WithSubagents fills this in from the parent's
	// agent.WithGate. Consumers calling NewSubagentTool directly pass
	// their own; nil leaves the tool ungated, which is what a host that
	// wired no gate anywhere already has everywhere else.
	Gate *permissions.Gate

	// ParentTracker is the usage.Tracker the DELEGATING agent bills
	// to. Every model turn the subagent takes is appended to it,
	// priced by the subagent's own model — so a delegated turn lands
	// in /usage, in /stats, and, most importantly, under the session
	// and per-turn cost ceilings.
	//
	// It has to be the parent's rather than Inner's, and it has to be
	// passed in rather than read off Inner, because a subagent
	// assembled declaratively is constructed with no tracker at all
	// (cmd/core-agent/subagents.go wires a model, a persona and a tool
	// surface, nothing else). Inner.Tracker() is nil in every
	// in-tree deployment.
	//
	// Why the tool has to do this itself: in library mode *Agent.Run
	// does not append usage — the harness consuming its event iterator
	// does (pkg/runner/headless.go, pkg/runner/wakeloop.go). A
	// subagent invoked as a tool runs on its OWN ADK runner inside
	// this handler, so its events never reach that iterator and no
	// harness can see them. The asynchronous door solves the same
	// problem the same way, one layer up
	// (pkg/agent/background/spawn.go rolls a spawned run into
	// parent.Tracker()).
	//
	// agent.WithSubagents fills this in from the parent's
	// agent.WithUsageTracker. Nil leaves the spend unaccounted, which
	// is what a host that wired no tracker anywhere already has
	// everywhere else.
	//
	// The roll-up is one level: it resolves at construction, so a
	// subagent that was itself built with WithSubagents bills ITS
	// subagents to whatever tracker IT held at the time — nil, for a
	// child assembled without one. That gap is library-only; the
	// declarative roster is flat (config.SubagentSpec has no nested
	// subagents, and a subagent is denied the spawn tool), so every
	// in-tree deployment is a single level.
	ParentTracker *usage.Tracker

	// Budgets bounds one delegation. Zero dimensions are unbounded,
	// which is what this door has always been.
	Budgets SubagentBudgets
}

SubagentOptions configures NewSubagentTool. Inner is required; everything else has sensible defaults.

type SubtaskBudgets

type SubtaskBudgets struct {
	// MaxTurns caps how many model turns the subtask may take.
	// Default is SubtaskDefaultMaxTurns. Subtasks are meant to be
	// single-purpose; >5 turns usually means the question is too
	// open-ended for a subtask and belongs in the parent's loop.
	MaxTurns int

	// MaxWallclock caps wall-clock duration. Default is
	// SubtaskDefaultMaxWallclock. Belt-and-suspenders against a
	// flaky model that streams forever without producing a final
	// turn.
	MaxWallclock time.Duration
}

SubtaskBudgets caps subtask cost in three dimensions. Whichever hits first wins. Returns SubtaskResult{Truncated: true} on budget exhaustion (not an error — caller's model can choose to retry with a wider budget or fall back to running the raw tool).

type SubtaskResult

type SubtaskResult struct {
	// Name echoes SubtaskSpec.Name for trace correlation.
	Name string

	// Digest is the model's final text output across all turns
	// (joined). Empty when Truncated is true and the model
	// produced no final-turn text before the budget hit.
	Digest string

	// Truncated reports that a budget (MaxTurns / MaxWallclock)
	// fired before the model emitted a final TurnComplete.
	// Digest may still hold partial text from earlier turns.
	Truncated bool

	// InputTokens / OutputTokens accumulate across every turn of
	// the subtask. Subtask cost rolls up to the parent's
	// usage.Tracker if WithUsageTracker is wired, so /stats
	// totals reflect everything.
	InputTokens  int
	OutputTokens int
	CostUSD      float64

	// CachedInputTokens / CacheCreationInputTokens are the two cache
	// buckets inside InputTokens, summed across the subtask's turns
	// and already clamped ([usage.TurnUsage.Clamped]).
	//
	// Reported rather than left implicit because CostUSD does not
	// survive every hop a subtask's numbers take. The MCP digest wrap
	// carries token counts across a JSON sidecar and re-prices on the
	// far side; without the buckets that re-pricing bills the whole
	// prompt at the uncached rate, which on Anthropic is wrong by up
	// to 25% and moves the session cost ceiling with it (#771).
	// Anything re-deriving cost from this struct should feed all six
	// numbers to [usage.Pricing.CostUSDForTurn] rather than reach for
	// CostUSD's two-argument sibling.
	//
	// CacheCreation1hInputTokens is the share of
	// CacheCreationInputTokens written at Anthropic's 1-hour
	// breakpoint TTL — a SUBSET of the write bucket, billed at 2x
	// base input rather than 1.25x (#770). Zero unless the operator
	// selected that TTL.
	CachedInputTokens          int
	CacheCreationInputTokens   int
	CacheCreation1hInputTokens int

	// ThoughtsTokens is the reasoning bucket, reported ADDITIVE to
	// OutputTokens and billed at the output rate (#927). Zero on
	// providers that fold thinking into their output count.
	//
	// It is the sixth number, and it is here for the same reason the
	// cache buckets are: on a small-tier thinking model a digest
	// subtask can spend more of it than it spends on candidates, so a
	// far side that re-prices from this struct without it charges the
	// calling session a fraction of what the subtask cost.
	ThoughtsTokens int

	// Duration is wall-clock time spent in the subtask. Useful
	// for the "fresh-context is fast" claim — typical subtask
	// completes in <2s on a flash-tier model.
	Duration time.Duration

	// TurnsUsed is the number of model turns the subtask actually
	// took. Useful for budget tuning ("am I hitting MaxTurns
	// often?").
	TurnsUsed int
}

SubtaskResult is what RunSubtask hands back. Digest is the distilled text the subtask's model produced; Truncated flags budget exhaustion so the caller can decide whether the partial answer is useful.

type SubtaskSpec

type SubtaskSpec struct {
	// Name attributes cost + traces. Required (non-empty) so the
	// audit log can distinguish subtask events; the subtask's
	// session-branch derives from this name.
	Name string

	// SystemPrompt is the subtask's role instruction. Since #459 it
	// COMPOSES: the subtask runs on the layered baseline
	// (CoreInstruction + provider quirks + AutonomousOverlay) with
	// this text appended, so it understands compaction summaries and
	// edit sequencing like any other agent. The parent's own
	// instruction is still NOT inherited (fresh context). Set
	// ReplaceSystemPrompt for the pre-#459 bare-prompt behavior.
	SystemPrompt string

	// ReplaceSystemPrompt, when true, makes SystemPrompt a full
	// replacement — no harness layers. Use only when supplying a
	// complete prompt of your own.
	ReplaceSystemPrompt bool

	// UserMessage is the question / instruction the subtask
	// receives. Treated as the operator's first user message in
	// the subtask's brand-new session.
	UserMessage string

	// Tools is the restricted set the subtask may call.
	// Typically a small read-only subset (read_file, grep,
	// fetch_url, etc.). Empty means tool-less.
	Tools []tool.Tool

	// Model overrides the parent's model for this subtask. Nil
	// (the default) means "use the parent's model." Wrapper tools
	// like AgenticReadFile typically point this at a smaller,
	// faster model (haiku-tier / flash-tier) so a "summarize this
	// file" subtask doesn't burn opus tokens.
	Model adkmodel.LLM

	// Budgets caps the subtask's resource use. Zero fields fall
	// back to SubtaskBudgetDefaults.
	Budgets SubtaskBudgets

	// SkipParentUsage suppresses the roll-up of this subtask's cost
	// into the *running* agent's usage.Tracker and ContextStats
	// counters. The subtask's own SubtaskResult still carries full
	// token + cost figures, so callers lose nothing.
	//
	// Set this when the agent executing the subtask is NOT the agent
	// that should be billed. The MCP digest wrap is the motivating
	// case (#717): its LLMFallback closure is bound to the primary
	// agent at boot, but it fires on behalf of whichever session made
	// the MCP call. Rolling up here would bill the primary session for
	// every other session's digests — and, because
	// maybeEnforceCostCeiling reads the tracker, could trip the
	// primary's cost ceiling on traffic it never generated.
	//
	// The correct session recovers the cost from the `savings` sidecar
	// riding on the tool result (pkg/agent/tool_savings_observer.go),
	// so suppressing here moves the charge onto the right books rather
	// than dropping it.
	SkipParentUsage bool
}

SubtaskSpec configures one RunSubtask call.

type TailVerdict added in v2.9.0

type TailVerdict struct {
	// InterruptedAt is the timestamp of the last committed event of the
	// broken turn. Zero when Interrupted is false.
	InterruptedAt time.Time

	// Interrupted reports whether the tail should be re-driven.
	Interrupted bool

	// InterruptedCalls lists the tool-call names of a mid-tool
	// interruption; nil for every other shape. See
	// ClassifyInterruptedTailWithCalls.
	InterruptedCalls []string

	// DeclineReason names why a tail that WOULD have been re-driven was
	// not, in a form fit for one stderr line. It is populated only for
	// the transient-error budget (#969) — every other terminal shape is
	// a completed or deliberately-killed turn, where declining is the
	// unremarkable answer and a log line would be noise. Empty when
	// Interrupted is true.
	//
	// It exists because the budget is the one stand-down an operator
	// cannot infer from the transcript: the session simply goes quiet,
	// which is indistinguishable from the agent having decided it was
	// done.
	DeclineReason string
}

TailVerdict is the full result of tail classification: what the two narrower wrappers above return, plus the operator-facing reason for the one decline that is a JUDGEMENT rather than a reading of shape (see DeclineReason). Callers that log — pkg/compose's triggers — take this form; callers that only branch can keep using the wrappers.

func ClassifyInterruptedTailVerdict added in v2.9.0

func ClassifyInterruptedTailVerdict(events []*session.Event) TailVerdict

ClassifyInterruptedTailVerdict is ClassifyInterruptedTail returning the full TailVerdict.

Directories

Path Synopsis
internal
subsession
Package subsession holds the session-derivation plumbing shared by the two subagent-spawning paths: the core parallel-tool-call path in pkg/agent (subagent.go) and the long-lived background path in pkg/agent/background.
Package subsession holds the session-derivation plumbing shared by the two subagent-spawning paths: the core parallel-tool-call path in pkg/agent (subagent.go) and the long-lived background path in pkg/agent/background.

Jump to

Keyboard shortcuts

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