agent

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: AGPL-3.0 Imports: 31 Imported by: 0

Documentation

Overview

The SDK imports are out-of-prefix; the gate filters them out of the in-prefix edge set (scripts/check_import_layers.py compute_edges), and the policy baseline is unchanged.

Package agent - completer adapter for the SDK-backed inner loop.

agentLoopCompleter adapts an internal/provider.Completer (CLI shape) to the mivia-ai-sdk/provider.Completer shape so the SDK-backed inner loop can drive today's per-provider implementations unchanged. The wrapper is unexported because only the dispatcher introduced by commit 3 ever instantiates it.

The wrapper is the minimum-viable adapter: Chat translates ChatTurn-or-Chat into one SDK Response, and ChatStream emits a single content chunk followed by one terminal chunk. True streaming and request-shape translation are tracked as follow-ups; the dispatcher gates them on commit 3.

Package agent - event translation for the SDK-backed loop.

The SDK loop publishes lifecycle events onto an events.Bus (name + string data); the CLI loop fans agent.Event values out to the caller's OnEvent callback and EventBus. The bridge subscribes one handler per mapped SDK event name and re-emits through the CLI's own emit helper, so session stamping, typed-bus publication, and agent attribution behave exactly as the legacy path's do.

Dropped by design, mirroring the legacy surface's droppedKinds precedent: iteration-end (the CLI has no per-iteration-end kind) and both heartbeat kinds (progress ticks have no CLI representation, and the SDK path leaves HeartbeatInterval at zero so they never fire anyway).

Package agent - SDK agent-loop run driver.

RunAgentLoopOnce drives the SDK's mivia-ai-sdk/agentloop.Loop (built by agentloop_adapter.go's buildAgentLoopOptions) for one turn: it applies the prompt-budget preflight, bridges the steer signals, runs the prompt-too-long recovery retry, and returns the SDK Result. It is ADDITIVE: the legacy (*Loop).Run in loop.go is unchanged, and the dispatcher's "sdk" branch (loop_dispatch.go) chooses the runtime.

Package agent - ref-only tool shim for the SDK backend.

The legacy CLI's refOnlyTier (internal/agent/shape_batch_refonly.go:25-45) is a per-result shaper that runs inside the batch shaper and spools a tool result to the CLI's *remainder.Spool when the tool is in Options.RefOnlyTools and the rendered body clears BatchDegradeFloorBytes. On the SDK path the SDK's own spool.SpoolTool is unreachable (it requires WithPrincipal on ctx, which no SDK call site attaches — see mivia-ai-sdk/spool/tool.go:38-42). The shim here is the agent-side substitute: each tool named in RefOnlyTools is wrapped with this per-call wrapper after the SDK registry conversion.

The shim's Run calls the inner SDK tool, then if the body is a string over the floor and the wrapper is on the named list, it calls the CLI's *remainder.Spool.Spool directly with the configured principal and returns the same ref-notice the legacy shape_batch produces. The spool type is internal/remainder.Spool, which lives downstream of internal/sdkadapter (it imports sdkadapter for sdkadapter.Mint), so the shim type itself belongs in the consumer package that bridges the two — not in sdkadapter, which would create an import cycle.

Package agent - dispatcher+cap tool shim for the SDK backend.

The legacy loop executes every tool call through Options.Dispatcher.Invoke (loop_tool_exec.go:40-47), which fires the parent's PreInvokeHook gate, the PostInvokeHook advisory, the dedup cache, and the policy checks, and then post-processes the outcome in buildExecResult: the MaxToolResultChars / capability cap, the remainder spool, and the hook-context attach. The SDK loop instead invokes the converted tool directly, which silently drops all of that. The shim here is the host-side substitute: each converted SDK tool is wrapped so its Run routes through the CLI dispatcher (or the raw CLI tool when no dispatcher is wired) and the result body gets the same cap/spool/hook-context treatment buildExecResult applies.

It is applied INNERMOST (before the ref-only shim and the turn shaping wrapper) so later shapers see the same capped body the legacy batch shaper sees.

Package agent - turn-level result shaping for the SDK backend.

The SDK's TurnResultBudget OMITS an over-budget result with a bare notice; the CLI's contract is the legacy batch shaper's three tiers (fit unchanged / re-cut with an honest notice / notice alone) and "no call may be failed by the budget". This wrapper carries that contract onto the SDK path host-side: every converted tool is wrapped once more (outermost), the turn's budget is charged through one shared counter, and each result is shaped with the legacy shapeOne tiers against the bytes remaining in the turn.

The SDK runs tool calls sequentially within a turn, so charging in call order is equivalent to the legacy batch-level allocation: at most one result straddles the boundary and pays the degrade floor. The D8 per-batch status line has no sequential analogue and is omitted; each degrade still carries its own honest notice, and a content-free heartbeat row is emitted per degraded result.

The wrapper runs OUTSIDE the ref-only shim, so a ref-only notice (already notice-sized) is charged as emitted. While this wrapper is active the adapter leaves Options.TurnResultBudget unset so the SDK's omission path never engages.

Index

Constants

View Source
const (

	// BatchDegradeFloorBytes is the smallest re-cut a straddling result gets.
	// Exported because the operator-facing minimum for the budget knob
	// (config.MinBatchResultBudgetBytes) is this number: a budget under the
	// floor cannot be honoured, and the two must not drift apart silently.
	// Cutting to exactly the remaining bytes is correct arithmetic and useless
	// output: a 40-byte remainder buys the model nothing but a notice. One
	// result per batch may overshoot the budget by up to this much - see the
	// bound in shapeReport's doc and §5 of the plan.
	BatchDegradeFloorBytes = 16 << 10
)
View Source
const DefaultToolTimeout = 60 * time.Second

DefaultToolTimeout is the agent-loop budget for tools that do not declare a Capability.Timeout. Finite so ordinary tools cannot hang the loop.

View Source
const SummaryMessageName = "context-summary"

SummaryMessageName is the Name a rendered context summary rides on. The summary is a USER-role message: every other trailing host injection in this repo (hook output framing in hook_context.go, parent context in internal/subagents/parent_inject.go) frames injected data as user-role content, and a trailing assistant message is a prefill/continuation hazard on Anthropic-style dialects - the model reads it as its own turn to continue. The Name lets hosts identify the injection when the wire keeps it; the header text carries the framing when the wire drops the Name.

View Source
const SummaryOutputLimitTokens = 1024

SummaryOutputLimitTokens is the default token cap for one summary request. OutputLimit is bounded above by 2048 in the summary validators.

512 was too tight for the summary this host's own prompt asks for: a reply populating every envelope field at the sizes the skeleton requests re-encodes to roughly 2KB - about 514 estimated tokens - so realistic summaries were rejected just past the bound and compaction silently degraded to structural-only, destroying the dropped context's only record. 1024 leaves real headroom while staying well under the validators' 2048 ceiling.

Variables

View Source
var ErrPromptBudgetExceeded = errors.New("prompt exceeds model budget")

ErrPromptBudgetExceeded means local history preparation could not fit the current request into the selected model's prompt budget.

Functions

func EmitCacheUsage

func EmitCacheUsage(ctx context.Context, opts Options, providerName, model string, usage provider.CacheUsage)

EmitCacheUsage publishes provider-reported prompt-cache accounting for one completion turn. It only publishes when the provider actually reported cache usage (usage.Reported) - a silent no-op otherwise, since most turns against a provider with capture disabled or with nothing to report carry no signal worth an event.

This only fires for turns that reach the agent loop: tool-enabled sessions and all subagent turns. A plain --no-tools chat session calls the provider directly via Completer.ChatStream and never reaches here; extending coverage there would require breaking ChatStream's public signature to carry structured usage back, which is out of scope for this feature.

func EmitCompaction

func EmitCompaction(ctx context.Context, opts Options, preparation contextmgr.Preparation, summarized bool, reason string)

EmitCompaction publishes the sealed, content-free progress event after the owning surface has durably committed the preparation. It is intentionally separate from emit so the generic event adapter cannot receive summary data. reason is only meaningful when summarized is false: the classified, content-free cause (see events.CompactionEvent.Reason). Callers pass "" when they have none to report.

This is the one Emit* reached while the caller still holds internal/chat's contextPublishMu, a session-wide lock also taken by /compact, session reset, and model switch - recordUsage itself stays synchronous here (matching EmitTokenUsage/EmitCacheUsage), but the concrete UsageWriter this repo wires in production (storage.usageWriter) dispatches its own write off this call's goroutine and tracks it against the store's own WaitGroup, so Record returns immediately without this function needing to know that.

func EmitTokenUsage

func EmitTokenUsage(ctx context.Context, opts Options, providerName, model string, usage provider.TokenUsage, estimatedTokens int, calibrationRatio float64)

EmitTokenUsage publishes provider-reported input/output token counts and estimate-vs-actual drift for one completion turn. It only publishes when the provider actually reported usage. This enables operators to see when the len(s)/4 heuristic diverges from real token accounting.

func FormatAskInject

func FormatAskInject(messageID, body string) string

FormatAskInject builds the model-visible text for a parent-routed ask so the target can answer with in_reply_to=<ask_id>. MessageID is required.

func FrameHookOutput

func FrameHookOutput(hookContext string) string

FrameHookOutput wraps one lifecycle hook's advisory text in the delimited block the model reads, neutralizing any tag the text tried to write. Blank input frames nothing.

It is exported so the wiring that actually runs hook scripts - which lives in internal/cli, on the other side of the dispatcher - can assert against this framing rather than against a copy of it. A test that reimplements the boundary it is checking only proves the copy agrees with itself.

func FrameParentMessage

func FrameParentMessage(body string) string

FrameParentMessage wraps parent steer text in paired delimiter tags with forged-tag neutralization (same pattern as FrameHookOutput, distinct tags).

func FrameParentMessages

func FrameParentMessages(bodies []string) string

FrameParentMessages concatenates multiple steer bodies into one framed user-role message (at most one frame per step).

func InjectSummaryMessage

func InjectSummaryMessage(messages []provider.Message, injected provider.Message) []provider.Message

InjectSummaryMessage appends injected at the END of an EPHEMERAL clone. Every structural message keeps its exact index, so an appended summary EXTENDS the provider prompt-cache prefix instead of splitting it (a mid-history insert would invalidate every cached block from the insertion point; see markStablePrefixCacheControl in internal/provider). The caller must never write the result back into loop history. InjectSummaryMessage appends injected as the last message, first dropping any message already carrying SummaryMessageName.

The drop makes injection idempotent per call, which matters specifically on the SDK backend: sdkPrepareTrim's Trim closure re-injects on every step of a multi-step turn, and the SDK's own run.go treats each Trim return as the run's real carried history (*history = trimmed) - so without this, a stale summary frame from an earlier step survives into a later step's messages as ordinary content, and this function would append a second, fresher copy beside it rather than replacing it, compounding by one frame per step for the rest of the turn. Injection is a single per-request frame by contract (findSummaryMessage/anyRequestCarriesSummary assume at most one); this keeps that true regardless of what the caller's slice already carries.

func NeutralizeHookTags

func NeutralizeHookTags(text string) string

NeutralizeHookTags removes any lifecycle-hook-output tags from text that originated in hook-authored content. The block reason a PreToolUse hook returns is untrusted text like any other hook output, but it reaches the model through a different path (the dispatcher's JSON status envelope) than advisory context (the framed block). Both paths must neutralize — the framed block uses this function directly, and the block-reason path uses the same pattern compiled in internal/runtime/hooks.go.

Exported so the wiring that actually runs hook scripts — which lives in internal/cli, on the other side of the dispatcher — can assert against the neutralization rather than against a copy of it.

func NeutralizeParentMessageTags

func NeutralizeParentMessageTags(text string) string

NeutralizeParentMessageTags strips forged parent-message tags from body text.

func RenderSummaryMessage

func RenderSummaryMessage(summary contextmgr.UntrustedSummary, omittedEvidence []string) provider.Message

RenderSummaryMessage renders a validated summary as a bounded user-role message named context-summary. The content is a factual, host-framed rendering of the sealed fields; ValidateSummary already refused the output if any field failed the redaction policy (INV-SEC-4 summaries-refuse behavior carries to injection). omittedEvidence is the host-side content-free diff of what the compaction dropped (request.Input.Evidence): it is rendered under the evidence label when the sealed summary carries no evidence of its own, so the model always sees what it can no longer read even when the provider does not echo the envelope's evidence list.

func RunAgentLoop

func RunAgentLoop(ctx context.Context, l *Loop, opts Options) (sdkagentloop.Result, error)

RunAgentLoop drives the SDK's agentloop.Loop for one Options. It requires a Loop carrying the completer and registry; a zero Loop fails closed at the nil-completer check in newAgentLoopCompleter.

func RunAgentLoopOnce

func RunAgentLoopOnce(ctx context.Context, l *Loop, opts Options, msgs []provider.Message) (sdkagentloop.Result, error)

RunAgentLoopOnce drives one SDK-backed agent-loop turn for the completer and registry carried by l, with CLI-shape opts and messages. It fail-closes on unsupported Options fields, converts the registry, bridges InterruptCh and MailboxPending onto a Steer, and returns the SDK Result of RunSteerable.

The steer bridge spawns at most two goroutines, both of which exit on ctx.Done: one resolves InterruptCh once and fires Trigger when the channel closes; one polls MailboxPending on a ticker (the WatchdogInterval when positive, else 250ms) and fires Trigger when the predicate returns true. A nil InterruptCh or MailboxPending spawns nothing.

func ScrubEphemeralToolMessages

func ScrubEphemeralToolMessages(messages []provider.Message, reg *tools.Registry)

ScrubEphemeralToolMessages runs after the final provider step, before a session adopts the turn. It preserves assistant/tool pairing while removing resource bodies from all subsequent history and persistence.

func SummaryFieldText

func SummaryFieldText(value string) string

SummaryFieldText bounds and sanitizes host text for a summary envelope field: invalid UTF-8 is replaced, control characters become spaces, and the value is truncated on a rune boundary to the field bound. The objective and state fields are capped by the same bound, so a pasted oversized user message stays envelope-valid instead of silently falling back.

func SummaryOverBudget

func SummaryOverBudget(afterTokens int, injected provider.Message, budget int) bool

SummaryOverBudget reports whether the structural retained request cost plus the injected message estimate exceeds the context budget. A non-positive budget is unbounded (no compaction bound, no injection bound).

func SummaryRequestBudget

func SummaryRequestBudget(budget int) int

SummaryRequestBudget returns a positive request budget for a summary request, falling back to a fixed default when the context budget is unset.

Types

type Event

type Event struct {
	Kind       EventKind
	ToolCallID string // stable correlation key for tool lifecycle events
	Name       string
	Detail     string
	Content    string
	Input      string // bounded, redacted tool input preview
	Output     string // bounded, redacted tool output preview
	// Denied is set only for EventHook: true when this run blocked its tool
	// call (a PreToolUse hook that denied). Renderers use it to give a
	// blocking run a distinct visual treatment from an advisory one.
	Denied bool
	// Program and Tool are set only for EventHook: the hook script's name
	// (not its path) and the tool it fired for. Name already carries the
	// hook's own event (PreToolUse/PostToolUse/Stop) for this kind, so these
	// are separate fields rather than an overload of an existing one.
	Program, Tool string
	// Origin attributes the event to the producing agent (zero = root loop).
	Origin EventOrigin
	// Identity is an optional typed runtime identity supplied by a routed
	// invocation. It contains no content or authorization material.
	Identity *events.Identity
	// Compaction is present only for the post-commit typed progress event. It
	// is not copied into generic content/input/output envelopes.
	Compaction *events.CompactionEvent
	// CacheUsage is present only for the typed prompt-cache accounting
	// event. It is not copied into generic content/input/output envelopes.
	CacheUsage *events.CacheUsageEvent
	// TokenUsage is present only for the typed token accounting event. It is
	// not copied into generic content/input/output envelopes.
	TokenUsage *events.TokenUsageEvent
}

type EventKind

type EventKind string
const (
	EventAssistant EventKind = "assistant"
	// EventToolPending is emitted BEFORE EventToolStart when a tool call
	// needs user approval. It is the only "pre-start" event: the loop
	// gates Dispatcher.Invoke on ApprovalGate when Emit returns, and
	// the resulting decision (approve / deny) determines whether
	// EventToolStart follows. Detail carries the execution class as
	// its string name so downstream consumers can route without
	// re-deriving from the registry.
	EventToolPending EventKind = "tool_pending"
	EventToolStart   EventKind = "tool_start"
	EventToolEnd     EventKind = "tool_end"
	EventStep        EventKind = "step"
	// EventHeartbeat is a wall-clock progress tick (model thinking, tool
	// batch, batch shaping). It is NOT a step: only real loop steps emitted
	// by emitStep may be EventStep, so consumers that budget or count steps
	// (e.g. subagent schema-retry step budgets) are not inflated by time.
	EventHeartbeat         EventKind = "heartbeat"
	EventPrune             EventKind = "prune"
	EventToolParallel      EventKind = "tool_parallel"
	EventSubagentStart     EventKind = "subagent_start"
	EventSubagentEnd       EventKind = "subagent_end"
	EventSubagentHeartbeat EventKind = "subagent_heartbeat"
	// EventSubagentDone is the run-level terminal signal for one subagent:
	// its loop returned and it will emit nothing further. Distinct from
	// EventSubagentEnd, which closes a single nested tool call - an agent
	// between two tool calls has no open tools but is very much still alive,
	// so only this event may retire it from the parent's live agent view.
	EventSubagentDone EventKind = "subagent_done"
	// EventThinking carries model reasoning (chain of thought) for providers
	// that expose it. Content is the reasoning delta.
	EventThinking EventKind = "thinking"
	// EventHook reports one lifecycle hook execution. Name is the event
	// (PreToolUse/PostToolUse), Detail names the script and what it decided,
	// and Output carries what it said. It is operator-facing only: the model's
	// copy of hook text travels in the tool result, framed.
	EventHook EventKind = "hook"
	// EventCompaction is emitted only after the context checkpoint commits.
	EventCompaction EventKind = "compaction"
	// EventCacheUsage carries provider-reported prompt-cache accounting for
	// one completion turn. See EmitCacheUsage.
	EventCacheUsage EventKind = "cache_usage"
	// EventTokenUsage carries provider-reported input/output token counts
	// for one completion turn. See EmitTokenUsage.
	EventTokenUsage EventKind = "token_usage"
	// EventWorkLimit is the soft conclude notice: the loop told the model to
	// wrap up because a work bound (deadline, output budget, or tool-call
	// budget) is close. It is observability only; the injected instruction
	// itself travels inside the provider request.
	EventWorkLimit EventKind = "work_limit"
	// EventSchemaRetry reports a subagent schema-validation corrective
	// re-entry that is ABOUT to happen: the previous reply failed schema
	// validation and runValidatedReply (internal/subagents/multi_step_schema.go)
	// is about to send a corrective turn and run a full new LLM turn. Without
	// this, a schema-repair retry ran with zero observable signal between the
	// first attempt's visible output and the retry's eventual completion -
	// indistinguishable from a stalled task. Detail carries a human-readable
	// "attempt N/M" message. Observability only: it does not count as
	// EventStep (must not inflate a schema-retry step budget) and must never
	// be confused with EventSubagentDone.
	EventSchemaRetry EventKind = "schema_retry"
	// EventEmptyResponseRetry reports a bounded empty-response retry that is
	// ABOUT to happen: the provider returned a genuinely empty response (no
	// text, no tool calls) and retryOnEmptyResponse
	// (internal/agent/agentloop_run.go) is about to re-run the whole SDK
	// completion loop from the same preparedMsgs. Without this, an
	// empty-response retry ran with zero observable signal - indistinguishable
	// from a stalled turn, the same silent-retry shape EventSchemaRetry fixes
	// for the subagent schema-repair retry. Detail carries a human-readable
	// "attempt N/M" message. Observability only: it does not count as
	// EventStep and does not alter retry control flow.
	EventEmptyResponseRetry EventKind = "empty_response_retry"
)

type EventOrigin

type EventOrigin struct {
	TaskID string // runtime request/task id - the attribution key
	Agent  string // dispatched subagent/skill name
	Depth  int    // nesting depth (root loop = 0)
	// TaskDescription is a bounded preview of the task this subagent was
	// given (see StampEventOrigin's caller), so a consumer attributing
	// events by TaskID can show what the subagent is actually doing without
	// having to separately correlate the initiating delegate/dispatch_tasks/
	// spawn_agent tool call's own Input. Empty for the root loop (zero
	// EventOrigin) and for any subagent kind that doesn't stamp origin at
	// all (a one-shot delegate has no nested tool calls to attribute).
	TaskDescription string
}

EventOrigin identifies the agent that produced an event. The zero value means the session's root loop. Subagent handlers stamp it (see subagents.StampEventOrigin) so nested tool events stay attributable to their run - without it, parallel agents are indistinguishable in the UI.

func (EventOrigin) IsZero

func (o EventOrigin) IsZero() bool

IsZero reports whether the origin is the root loop.

type Loop

type Loop struct {
	Completer provider.Completer
	Tools     *tools.Registry
	Messages  []provider.Message
	// LastPreparation is retained only after the final provider request
	// succeeds. The owning chat surface commits it; the loop never publishes.
	LastPreparation contextmgr.Preparation
	HasPreparation  bool

	// PreparationErr records an interrupted recovery failure so the session can
	// surface the real cause instead of misreporting a checkpoint conflict.
	PreparationErr error
	// LastFinishReason is the provider finish reason of the last successfully
	// completed step of the most recent Run: "stop", "tool_calls", "length",
	// or another provider vocabulary value. Empty when no step completed. The
	// schema-repair loop reads it to distinguish a reply truncated by the
	// output budget (finish_reason "length") from ordinary invalid JSON, so it
	// can say so honestly instead of re-prompting with the same budget.
	LastFinishReason string
	// Calibration tracks the rolling EWMA correction ratio between estimated
	// and provider-reported token usage. It is updated after every successful
	// provider response that reports usage, and its Ratio is passed to
	// context planning and token usage events. The zero value (Ratio=0) is
	// safe: applyCalibration treats 0 as 1.0 (no correction).
	Calibration contextmgr.Calibration

	// TurnState accumulates bounded, content-free host facts (omitted-message
	// evidence, tool names, changed surfaces, risks, latest assistant state)
	// for the summary envelope of the current run. Reset at Run start; it
	// never leaves the loop and is never consulted by planning, commit, or
	// checkpoint fingerprinting.
	TurnState *contextmgr.TurnState
	// contains filtered or unexported fields
}

func (*Loop) InjectedSummary

func (l *Loop) InjectedSummary() (provider.Message, bool)

InjectedSummary returns the summary message this run last injected into a provider request, and whether there was one. The owning surface appends it to the turn's committed active context so it survives the turn boundary; the loop itself never writes it into l.Messages, which must stay structural so planning, idempotency, BaseDigest, and checkpoint bytes are untouched.

func (*Loop) Run

func (l *Loop) Run(ctx context.Context, userText string, opts Options) (string, error)

func (*Loop) SummaryFailureReason

func (l *Loop) SummaryFailureReason() string

SummaryFailureReason returns the classified reason why this turn produced no context summary, or empty if the summary was successfully injected or no summarizer was configured.

func (*Loop) TurnCompactionEmitted

func (l *Loop) TurnCompactionEmitted() bool

TurnCompactionEmitted reports whether compaction was emitted mid-turn during step preparation.

type Options

type Options struct {
	Model       string
	Temperature *float64
	MaxTokens   *int
	// AdvertisedToolSpecs, when non-nil, is the tools[] array Run serializes
	// on every step of this turn - the host's pinned, binding-lifetime
	// snapshot (plan tools-advertising/01), computed once from the session's
	// admissible union and byte-identical across turns and admissions. Run
	// falls back to Tools.OpenAITools() when nil, which is today's behavior:
	// subagent and workflow-engine loops that never set this field are
	// unaffected.
	AdvertisedToolSpecs []provider.ToolSpec
	// Reasoning is the selected model's reasoning dial, carried onto every
	// request this loop makes. Its zero value sends nothing.
	Reasoning  reasoning.Setting
	MaxSteps   int
	WorkLimits runtime.WorkLimits
	// PreserveWorkLimits keeps cumulative reservations across a corrective
	// re-entry of the same task invocation.
	PreserveWorkLimits    bool
	DisableProviderReplay bool
	// MaxContextTokens sets the approximate token limit for the prompt context.
	// Pruning is hysteretic, mirroring contextmgr.Plan: history is left
	// untouched below 80% of the budget, and once that trigger is crossed old
	// turns are dropped (keeping system prompt and recent turns) down to ~50%,
	// so one provider prompt-cache miss buys many cache hits before the next.
	// 0 or negative means no pruning.
	MaxContextTokens int
	// MaxToolResultChars caps each tool result stored in conversation history,
	// in BYTES despite the name (it bounds len() of the UTF-8 body; see
	// capToolResult). This prevents a single large output (e.g. read_file of
	// 256KB) from exceeding the context budget. 0 means no cap (use full
	// result); per-tool Capability.MaxResultBytes budgets still apply. To bound
	// one oversized result against the prompt budget while keeping full results
	// the default, set BatchResultBudgetBytes < 0 (derived batch budget)
	// instead.
	MaxToolResultChars int
	// BatchResultBudgetBytes bounds the bytes ONE tool batch may add to
	// history, across all its parallel calls together. Per-call caps cannot
	// see each other, so N calls each honestly under its own cap still blow
	// the context when they land in the same step; this is the only bound that
	// sees the batch as a whole.
	//
	// 0 (the default) disables the mechanism entirely - shapeBatch is not
	// invoked and the append path is byte-identical to having no budget.
	// Negative derives it from MaxContextTokens (inert when that is unset).
	// Positive is the literal byte budget. Scope is one runToolBatch: nothing
	// is charged across compaction boundaries, where cross-batch growth is
	// already compaction's job.
	BatchResultBudgetBytes int
	// RefOnlyTools lists tool names whose results are never inlined into the
	// model context when they exceed BatchDegradeFloorBytes; instead the whole
	// body is spooled to RemainderSpool and the notice names a remainder ref
	// (read_output) that the model can fetch. Empty/absent names keep normal
	// degrade behavior.
	RefOnlyTools         []string
	MaxToolCallsPerBatch int
	MaxConcurrentTools   int
	ToolTimeout          time.Duration
	// ToolRunTimeout is the SDK tool-registry's registry-wide run-timeout
	// backstop for tools that declare no Capability.Timeout (the [tools]
	// tool_run_timeout_seconds knob). <= 0 (the default) maps to the SDK's
	// TimeoutNone: no registry-wide cap, because the dispatcher shim
	// already arms every call's Capability.Timeout / ToolTimeout as a real
	// deadline and the SDK backstop must never be tighter than those
	// declared budgets. Positive is the literal bound.
	ToolRunTimeout time.Duration
	RequestTimeout time.Duration
	ParentID       string
	TurnID         string
	SessionID      string
	Role           string
	Depth          int
	// Step is the loop's 1-based model-step index, stamped per step on the
	// loop's own Options copy before tool calls are dispatched (plan:
	// step-scoped tool dedup). 0 means unset/legacy.
	Step       int
	Budget     int
	Dispatcher *runtime.Dispatcher
	// StagedToolMessage returns the denial message for a tool call to a name
	// staged for loading (load_tools) but not yet published to the live tool
	// surface, plus true. The loop calls it only when the name is absent from
	// its registry. The returned message announces why publication is pending;
	// nil means no check and the generic denial message is used. It must be
	// safe for concurrent calls.
	StagedToolMessage func(name string) (string, bool)
	// UnadmittedToolHandler is checked when a call names a tool absent from
	// the live registry AND StagedToolMessage found no pending stage for it.
	// It lets the host recognize a tool that IS advertised (plan
	// tools-advertising/01: the wire tools[] array now includes every
	// deferred candidate, not just admitted ones) but not yet admitted for
	// execution: auto-stage it for native publication at the next step
	// boundary (so later calls in the turn need no special handling), AND
	// serve THIS call synchronously against the full authorized tool set
	// when possible, so the model never sees an error for a call it already
	// made correctly. Handled=false means the name is not recognized at all
	// (a hallucinated tool) and the caller falls through to the generic
	// denial. Nil disables the check. Must be safe for concurrent calls.
	UnadmittedToolHandler func(ctx context.Context, name string, args json.RawMessage) UnadmittedToolResult
	// ApprovalGate is the synchronous user-approval bridge for tool calls.
	// It is invoked by executeToolTask and by the SDK-path approval wrapper
	// before Dispatcher.Invoke for any tool whose capability.Class >=
	// tools.ExecutionWrite; tools of class ExecutionRead or Unclassified
	// skip the call. The function MUST be safe to call concurrently from
	// multiple goroutines (parallel tool batches). A nil gate is equivalent
	// to "approve every call": the loop runs as if there were no approval
	// surface at all, matching pre-Phase-4 behavior.
	ApprovalGate func(ctx context.Context, name string, args json.RawMessage) sdkadapter.ApprovalResult
	// ApprovalStanding is consulted BEFORE ApprovalGate to honor "always"
	// decisions ("a always" / "D deny always"). It is keyed on tool name
	// and carries a verdict (approved or denied) plus a class tag so the
	// same call short-circuits the gate for the rest of the session.
	// Nil is safe: every call falls through to ApprovalGate. The same
	// instance backs the SDK-path wrapper so a "always" decision persists
	// across legacy and SDK turns within one session.
	ApprovalStanding *sdkadapter.ApprovalStanding
	// ApprovalPolicy controls tool execution approval policy ("write-only", "auto" / "never" [yolo], "always").
	ApprovalPolicy string
	// RemainderSpool, when non-nil, stores truncated tool-result bodies under
	// content refs so the model can page them via read_output. Nil means
	// truncation notices omit refs (legacy plain notices).
	RemainderSpool *remainder.Spool
	OnEvent        func(Event)
	EventBus       *events.Bus // publishes agent events to extensible delivery
	// EventIdentity is a validated public identity snapshot for this turn.
	EventIdentity *events.Identity
	// UsageWriter, when non-nil, durably records token/cache/compaction usage
	// measurements alongside the existing EventBus publish. Nil keeps usage
	// events exactly as ephemeral as they are today (subagent/workflow-engine
	// loops that never set this field are unaffected).
	UsageWriter usage.UsageWriter
	FinalWriter io.Writer
	// RequireFinalText fails a turn that produced no assistant text anywhere
	// instead of reporting an empty success. Interactive surfaces set it: a turn
	// that renders as "done" with no answer is indistinguishable from the agent
	// stopping for no reason. Sub-agents leave it false, because buildResult
	// discards a task's output whenever its error is non-nil, and a task that
	// did its work through tools and then stopped without prose did succeed.
	RequireFinalText bool
	// PreparationManager is an optional root-owned preparation capability. It
	// has no checkpoint publisher and is therefore safe to pass to nested loops.
	PreparationManager contextmgr.PreparationManager
	PreparationInput   contextmgr.PrepareInput
	// SummaryConfig wires the optional LLM summarizer into the request path. A
	// nil Summarizer keeps the loop structural-only: no summary provider call,
	// no injected message, byte-identical requests. Redaction is the host's
	// compiled redaction policy applied to summary input and output through
	// the summary validators.
	SummaryConfig SummaryConfig
	// BeforeStep, when set, is called on the loop goroutine at the top of each
	// step before history pruning and request build (plan 53.03). Returned
	// messages are appended to the loop history. Nil is a no-op.
	BeforeStep func() []provider.Message
	// InterruptCh, when non-nil, resolves the channel a parent can signal to
	// softly interrupt the in-flight LLM call (plan 54). It is re-read once
	// per LLM call. Nil disables the signal path. A steer never cancels a tool
	// batch: only the LLM-scoped context is cancelable.
	InterruptCh func() <-chan struct{}
	// MailboxPending, when non-nil, reports whether ANY message is waiting in
	// the mailbox. The watchdog path cancels only when it returns true, so a
	// stale signal after a drain can never cancel a call. The interrupt-signal
	// path uses the stricter MailboxPendingInterrupt, so a stale signal paired
	// with a later non-interrupt message is never a cancel.
	MailboxPending func() bool
	// MailboxPendingInterrupt, when non-nil, reports whether an
	// Interrupt-flagged steer is queued. The watcher's signal branch cancels
	// only when it returns true; the watchdog branch keeps gating on
	// MailboxPending (any pending message bounds non-urgent steer latency).
	// Nil disables the signal gate.
	MailboxPendingInterrupt func() bool
	// WatchdogInterval bounds steer latency when no interrupt signal is wired:
	// with a steer pending, the in-flight LLM call is softly interrupted at
	// most this often. 0 disables the watchdog.
	WatchdogInterval time.Duration
	// SoftInterruptCooldown caps soft-interrupt frequency across calls: at
	// most one interrupt per window. 0 disables the cooldown (tests). The
	// production 5s default lives in the subagents wiring, NOT here.
	SoftInterruptCooldown time.Duration
	// Surface, when non-nil, is invoked by Loop.Run at the top of EVERY step
	// iteration (before runStep, hence before BeforeStep inside prepareStep) to
	// fetch that step's host surface. Non-nil fields of the returned Surface
	// are applied to that step only; nil fields leave the loop's own state
	// unchanged. The host must supply Registry, Dispatcher, and ToolSpecs from
	// one consistent read so the registry/dispatcher/spec agreement invariant
	// (M3) holds for the step. Nil is a no-op.
	Surface func() Surface
}

type SummaryConfig

type SummaryConfig struct {
	// Summarizer is the captured provider/model/policy binding. Nil disables
	// summary injection entirely.
	Summarizer *contextmgr.Summarizer
	// UnavailableReason names the setup-time failure that prevented a Summarizer
	// from being wired, when Summarizer is nil.
	UnavailableReason string
	// Redaction is the host's compiled redaction policy. It classifies every
	// envelope field and every provider output before anything reaches the
	// wire or storage.
	Redaction contextstate.RedactionPolicy
}

SummaryConfig is one turn's immutable summary wiring. It is read, never written, by the loop, mirroring Options itself.

type Surface

type Surface struct {
	Registry       *tools.Registry
	Dispatcher     *runtime.Dispatcher
	ToolSpecs      []provider.ToolSpec
	RemainderSpool *remainder.Spool
}

Surface is one step's host-supplied tool surface: the registry the loop dispatches against, the runtime dispatcher for per-turn dedup, the tool specs published to the model for the step, and the spool for remainder refs. Fields come from one consistent host read; a zero field means "keep the loop's current value for this step".

type UnadmittedToolResult

type UnadmittedToolResult struct {
	// Handled is false when the handler has no opinion on this tool name at
	// all (not advertised, a hallucinated name); the caller falls through to
	// its own generic "not available" denial. True for every other case.
	Handled bool
	// Ran is true when the tool was actually executed synchronously and
	// Content is its real, successful result: the caller must render it
	// exactly like an ordinary successful tool call - no "error: " prefix,
	// no failed tool_end, no denial framing anywhere the model or the
	// operator can see. False means Content is a human-readable denial
	// reason instead (e.g. staged but could not run synchronously); the
	// caller applies its own "error: " framing as before.
	Ran bool
	// Content is the tool's real result (Ran) or the denial text (!Ran).
	Content string
	// HookRuns are the lifecycle hooks that executed for this call, for the
	// OPERATOR's view. Set on the Ran path AND on a !Ran path whose cause
	// was a PreToolUse block - the case an operator most needs to see, since
	// it is the run that stopped the call. It must be nil for a dedup-served
	// duplicate: a duplicate is answered with the OWNER's runs (DC-9), which
	// did not execute for THIS call, so reporting them would show a hook
	// firing that never fired here. Nil when the handler never reached the
	// dispatcher at all.
	HookRuns []runtime.HookRun
	// HookContext is the advisory text lifecycle hooks produced for this
	// call, for the MODEL. The caller frames it through appendHookContext,
	// so it gets the same delimiting and tag-neutralization the ordinary
	// dispatcherShim.Run path applies. Unlike HookRuns it IS set for a
	// dedup-served duplicate: DC-9 answers a duplicate with the owner's
	// post-hook Result, and the shim appends that context too. Set but
	// currently unused on a !Ran (denial) result: the denial text is left
	// unchanged until a PreToolUse block's own advisory context is threaded
	// too (see runDeferredToolNow's doc comment).
	HookContext string
}

UnadmittedToolResult is returned by Options.UnadmittedToolHandler.

Jump to

Keyboard shortcuts

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