Documentation
¶
Overview ¶
Package domain is the ubiquitous language (CONTEXT.md) rendered as Go: every type, interface, enum, sentinel error, and hook working-value in Apogee's public surface, plus the pure logic intrinsic to those types (the Mechanism registry's ordering-cycle detection, ConfinementCaps.AutoEligible, the Session envelope and its versioning).
It is the foundational layer of the package layout decided in ADR 0010: the engine (internal/agent), the provider (internal/provider), and the platform backends (internal/platform) all import domain for these types and never import the root apogee package; the root facade re-exports the public ones as aliases. Domain depends only on the standard library, so the language has a dependency-free home and the invariant "internal/* never imports root" holds at the bottom of the graph.
Naming: "domain", not "core" — the retired "Apogee Core" library term (CONTEXT.md "Retired terms") is unrelated to this internal package.
Index ¶
- Constants
- Variables
- func IsReadOnly(t Tool) bool
- func IsSubprocessTool(t Tool) bool
- func PromptChars(msgs []Message, tools []ToolDef) int
- func WithConfinement(ctx context.Context, conf Confinement) context.Context
- type ApprovalDecision
- type ApprovalEvent
- type ApprovalRequest
- type Approver
- type AskAnswer
- type AskRequest
- type Asker
- type AuditEvent
- type Budget
- type Capability
- type Config
- type Confinement
- type ConfinementBox
- type ConfinementCaps
- type Confiner
- type ContextConfig
- type Conversation
- func (c *Conversation) Append(m Message)
- func (c *Conversation) AssistantBoundaries() []int
- func (c *Conversation) At(i int) Message
- func (c *Conversation) ClearDeferred()
- func (c *Conversation) Defer(inject string)
- func (c *Conversation) DeferredLen() int
- func (c *Conversation) DropRange(start, end int)
- func (c *Conversation) Insert(i int, m Message)
- func (c *Conversation) Len() int
- func (c *Conversation) MarshalJSON() ([]byte, error)
- func (c *Conversation) Messages() []Message
- func (c *Conversation) PrefixEnd() int
- func (c *Conversation) Range(fn func(i int, m Message) bool)
- func (c *Conversation) Replace(msgs []Message)
- func (c *Conversation) Revision() int
- func (c *Conversation) SetMessageContent(i int, content string)
- func (c *Conversation) TakeDeferred() (injects []string, ok bool)
- func (c *Conversation) TruncateDeferred(n int)
- func (c *Conversation) UnmarshalJSON(data []byte) error
- type ConversationView
- type ErrorEvent
- type Event
- type EventBase
- type EventSink
- type ExchangeView
- type ExternalEffectKind
- type ExternalEffectTool
- type ExternalEffects
- type FingerprintConfidence
- type FingerprintResolver
- type FinishReason
- type HistoryRewriter
- type HookPoint
- type LoopView
- type Mechanism
- type MechanismDescriptor
- type MechanismFiredEvent
- type MechanismID
- type MechanismRegistry
- func (r *MechanismRegistry) Add(m Mechanism) error
- func (r *MechanismRegistry) AddExperimental(at HookPoint, hook any) error
- func (r *MechanismRegistry) Experimental(at HookPoint) []any
- func (r *MechanismRegistry) Ordered(at HookPoint) []Mechanism
- func (r *MechanismRegistry) ValidateIncompatibilities() error
- func (r *MechanismRegistry) ValidateOrdering() error
- func (r *MechanismRegistry) ValidateRequirements() error
- type Message
- type MessageEvent
- type Mode
- type ModelFingerprint
- type ModelProfile
- type OrderingConstraints
- type PostResponseAction
- type PostResponseDecision
- type PostResponseHook
- type PostToolResultHook
- type PreRequestHook
- type PreToolExecHook
- type PresentMethod
- type PresentOutcome
- type PresentRequest
- type Presenter
- type ReadOnlyTool
- type ReasoningEvent
- type Request
- func (r *Request) AppendSupersededAssistant(text string, calls []ToolCall)
- func (r *Request) AppendToSystem(marker, text string) (injected bool)
- func (r *Request) Extra(key string) (json.RawMessage, bool)
- func (r *Request) InjectContext(text string)
- func (r *Request) Model() string
- func (r *Request) Revision() int
- func (r *Request) SetDepth(depth int)
- func (r *Request) SetExtra(key string, v json.RawMessage)
- func (r *Request) SetMessageContent(index int, content string)
- func (r *Request) SetSampling(p SamplingParams)
- func (r *Request) SetTools(tools []ToolDef)
- func (r *Request) State() RequestState
- func (r *Request) View() LoopView
- type RequestState
- type ResolvedSkill
- type Response
- func (r *Response) AppendToolCall(call ToolCall)
- func (r *Response) FinishReason() FinishReason
- func (r *Response) Revision() int
- func (r *Response) SetText(s string)
- func (r *Response) SetToolCallArguments(index int, args json.RawMessage)
- func (r *Response) Text() string
- func (r *Response) Thinking() (text string, ok bool)
- func (r *Response) ToolCalls() []ToolCall
- func (r *Response) View() LoopView
- type Role
- type SamplingParams
- type Session
- type SkillResolver
- type StepResult
- type StepStatus
- type StreamResetEvent
- type SubprocessTool
- type SuppressionPolicy
- type ThinkingProfile
- type ThinkingStyle
- type TokenEvent
- type Tool
- type ToolCall
- type ToolCallEvent
- type ToolCallFormat
- type ToolDef
- type ToolRegistry
- type ToolResult
- type ToolResultEvent
- type UsageEvent
- type UserInput
Constants ¶
const SessionVersion = 1
SessionVersion is the schema version Snapshot stamps and Resume/DecodeSession accept. v1 is finalised by P1.6; a snapshot whose Version exceeds this is from a newer build and is rejected with ErrSessionVersion (ADR 0001 — no silent forward migration).
Domain owns the Session *envelope* and its versioning; the engine (internal/agent) owns the opaque State payload and its schema. In v1 that payload is the loop's full quiescent-boundary state: the conversation (messages with tool-call/result pairing and per-message Extra wire fields, plus the deferred-action queue) and the loop counters (turnIndex, inExchange, pending input) — see internal/agent/state.go.
Variables ¶
var ( // at all (cfg.Confiner == nil) — Auto has no facility to enforce its subprocess surface // with, so it is refused. A PRESENT-but-incapable backend (denyConfiner, or landlock on // a kernel <5.13) does NOT refuse Auto: under ADR 0012 the gate is FSWrite-only and the // unfenceable subprocess surface falls back to Approval — "confine if you can, gate if // you can't" (confinement-execution-contract §4–5). The CLI always injects a backend, // so this error is a library-embedder contract, not a CLI-user-facing one. ErrAutoUnavailable = errors.New("apogee: auto mode requires filesystem-write confinement, unavailable on this host") // safety net (ADR 0012; confinement-execution-contract §2.2): a Confiner backend // that finds it cannot establish the requested box for a subprocess returns it, and // the dispatch disposition falls back to Approval rather than running the call // unconfined. Distinct from ErrAutoUnavailable, which gates Auto at construction. ErrConfinementUnavailable = errors.New("apogee: confinement unavailable on this host") // ErrOrderingCycle is returned by New / registry Add when Mechanism ordering // constraints form a cycle — it must fail loudly at startup (ADR 0003). ErrOrderingCycle = errors.New("apogee: mechanism ordering constraints contain a cycle") // ErrIncompatibleMechanisms is returned by New when two registered Mechanisms declare // each other incompatible (MechanismDescriptor.IncompatibleWith) — they must never // co-fire, so registering both is a configuration error that fails loudly at startup // (ADR 0003), the same posture as ErrOrderingCycle. ErrIncompatibleMechanisms = errors.New("apogee: incompatible mechanisms registered together") // ErrMissingRequirement is returned by New when a registered Mechanism declares a required // peer (MechanismDescriptor.Requires) that is not itself registered — the two are benched as // a stack, so enabling one without the other is a configuration error that fails loudly at // startup (ADR 0003 posture, ADR 0014 §4), the dual of ErrIncompatibleMechanisms. ErrMissingRequirement = errors.New("apogee: a required mechanism is not registered") // ErrUnknownMechanism is wrapped by mechanisms.Build (and, through it, agent construction from // Config.EnableMechanisms) when a named Mechanism ID is not in the catalogue — a typo'd or // deferred ID fails loudly rather than silently disabling a Mechanism (ADR 0015 §4). The // wrapping error still names the known IDs; this sentinel makes the condition matchable with // errors.Is (locked decision 5). ErrUnknownMechanism = errors.New("apogee: unknown mechanism") // ErrSessionVersion is returned by Resume / DecodeSession for a snapshot whose // schema version this build does not understand. ErrSessionVersion = errors.New("apogee: unsupported session schema version") // ErrInputPending is returned by Submit when an Exchange is already in progress. ErrInputPending = errors.New("apogee: cannot submit input mid-exchange") // ErrDuplicateTool is returned by ToolRegistry.Register when a tool with the same // Name is already registered — the name is the model's stable handle, so a // collision is a configuration error, not a silent overwrite. ErrDuplicateTool = errors.New("apogee: a tool with this name is already registered") // ErrInvalidTool is returned by ToolRegistry.Register for a tool that cannot be // addressed — currently an empty Name. ErrInvalidTool = errors.New("apogee: invalid tool") )
Functions ¶
func IsReadOnly ¶
IsReadOnly reports whether t has declared itself read-only via ReadOnlyTool. A tool that makes no such declaration is treated as write-capable.
func IsSubprocessTool ¶
IsSubprocessTool reports whether t has declared itself a subprocess tool via SubprocessTool — the signal the disposition confines it (Auto) rather than gating it. A tool that makes no such declaration is not a subprocess tool.
func PromptChars ¶
PromptChars is a stable character measure of a request's prompt — the message contents and tool-call arguments plus the tool menu's names, descriptions, and schemas — used both as the estimator's calibration sample (internal/context.TokenEstimator.Calibrate) and as the basis for a token estimate (EstimateTokens). It deliberately omits the chat template's own markup, which the character count cannot see; the same omission on both sides of the chars→token ratio means a systematic offset cancels, so an estimate stays consistent with the calibration that produced the ratio.
func WithConfinement ¶
func WithConfinement(ctx context.Context, conf Confinement) context.Context
WithConfinement returns a context carrying conf, so a subprocess tool's Execute can retrieve the Confiner + box to confine the command it launches. The dispatch disposition calls this only when it has decided to run a subprocess tool confined.
Types ¶
type ApprovalDecision ¶
type ApprovalDecision string
ApprovalDecision is the Approver's verdict.
const ( ApprovalAllow ApprovalDecision = "allow" ApprovalDeny ApprovalDecision = "deny" ApprovalAllowForSession ApprovalDecision = "allow-for-session" )
type ApprovalEvent ¶
type ApprovalEvent struct {
EventBase
Request ApprovalRequest
Decision ApprovalDecision
}
ApprovalEvent reports that an Approval was requested/decided for a tool call. (The decision is obtained synchronously via the Approver; this event is for observers — TUI display, bench accounting.)
type ApprovalRequest ¶
type ApprovalRequest struct {
Tool string
Arguments json.RawMessage
Reason string // why approval is required (e.g. "write", "unconfinable MCP tool")
}
ApprovalRequest describes the pending tool call the human is asked to allow.
type Approver ¶
type Approver interface {
Approve(ctx context.Context, req ApprovalRequest) (ApprovalDecision, error)
}
Approver is the host-supplied human-in-the-loop gate on a single tool call. In Ask-Before mode it is consulted for every call; in Auto mode it is consulted only for tools that cannot be confined (e.g. MCP — ADR 0004). It is called synchronously inside a Step and may block on the human; cancelling ctx unblocks it.
type AskAnswer ¶
type AskAnswer struct {
// Text is the human's typed answer.
Text string
}
AskAnswer is the human's free-text reply. A STRUCT for the same freeze-safety reason (a post-v1 Choice index is an additive field).
type AskRequest ¶
type AskRequest struct {
// Question is the free-text prompt the human answers.
Question string
}
AskRequest is the free-text question put to the human. It is a STRUCT (not a bare string) for freeze-safety (D7): a post-v1 multiple-choice field (Choices) is then an additive, non-breaking change to the v1.0.0 surface.
type Asker ¶
type Asker interface {
Ask(ctx context.Context, req AskRequest) (AskAnswer, error)
}
Asker is the host-supplied delegate the ask_user tool routes a free-text question to mid-task: the model asks the human a clarifying question and waits for a typed answer. It is the public analogue of Approver (a deliberate v1-surface addition, D7) but for free-text Q&A, NOT a safety gate — an Asker decision carries no allow/deny semantics and never bypasses the Approval/disposition machinery. It is consulted synchronously inside a Step (on the worker goroutine) and may block on the human; cancelling ctx unblocks it.
In a headless / non-interactive context the host must supply an Asker that FAILS SAFE (returns promptly with an error or a scripted answer) rather than hanging — and a nil Asker means the ask_user tool is simply not registered (graceful), so the model is never offered a question it cannot have answered.
type AuditEvent ¶
type AuditEvent struct {
EventBase
Tool string
CallID string
Decision string
Reason string // the guardrail reason, if any
IsError bool // whether the recorded result was a tool-level error
}
AuditEvent surfaces one append-only audit record — a tool call, the guardrail decision it cleared/was blocked by, and whether its result errored — to the EventSink as it is recorded, so the audit trail is OBSERVABLE (and snapshot- or log-shippable) rather than living only in a volatile in-process ring no observer reads (security-review M1). Because a sub-agent emits through the parent's EventSink at Depth > 0, a delegated call's audit record reaches the same observer at its nesting depth instead of vanishing with the discarded child Agent.
The payload mirrors security.AuditRecord but is expressed in domain-only types: the agent layer (which imports both domain and security) constructs it, so domain keeps its no-upward-dependency property (ADR 0010). Decision is the audit decision as a string (e.g. "allowed", "dangerous-refused", "circuit-tripped").
type Budget ¶
type Budget struct {
ContextLimit int // the model's full context window (n_ctx tokens); 0 when unknown
Used int // tokens the last server usage reported the prompt occupied; 0 until the first UsageEvent
CharsPerToken float64 // the chars→token ratio, calibrated against reported usage
// The window allocation (internal/context.Allocate): how many tokens of ContextLimit each
// part of a request may claim. ResponseReserve is held back for the reply; the rest is split
// across SystemPrompt, FileContext, and History (they sum to ContextLimit - ResponseReserve).
// Every field is 0 when the window is unknown. It is ADVISORY: the context reducers
// (tool-result capping, automatic Compaction) read it; nothing in the request path is
// reshaped by it here.
ResponseReserve int
SystemPrompt int
FileContext int
History int
}
Budget is the read-only context-budget view a hook reads to gate token-sensitive behaviour (e.g. Library injection backs off as the window fills, tool-result capping trims a result to its fraction). It is the CONTEXT "Budget": the single authority on how much room each part of a request gets. Its token accounting is calibrated against server-reported usage (internal/context.TokenEstimator), so Used and CharsPerToken are honest measures rather than a fixed guess.
func (Budget) EstimateTokens ¶
EstimateTokens converts a character count to a token estimate through the calibrated chars→token ratio, rounding up so a part is never estimated to fit when it is one token over. A non-positive CharsPerToken — the zero-value Budget of an uncalibrated view — yields 0, so a comparison against any positive threshold stays false: token-gated behaviour is inert until the ratio is known, never fired on an un-measured guess.
func (Budget) HistoryExceedsAllocation ¶
HistoryExceedsAllocation reports whether the estimated token size of msgs (the conversation history the reducers reclaim) has outgrown the Budget's History allocation. It is the single compare behind both the engine's automatic Compaction trigger and any hook reading the Budget, so the two can never disagree. The measure runs the whole conversation through the calibrated ratio (PromptChars omits the tool menu — that is not history) and is deliberately conservative: comparing the whole conversation against the History slice trips slightly before the prompt would overflow. A non-positive History (the window is unknown, so nothing was allocated) never trips — there is no basis to bound.
type Capability ¶
type Capability string
Capability is what a Mechanism does — and what Bypass switches on (ADR 0006: Bypass disables proactive-nudge + response-repair, keeps off-ramp).
const ( CapOffRamp Capability = "off-ramp" // exempt recovery guarantee; survives Bypass CapProactiveNudge Capability = "proactive-nudge" // disabled under Bypass CapResponseRepair Capability = "response-repair" // disabled under Bypass )
type Config ¶
type Config struct {
// Upstream — the local OpenAI-compatible LLM server (CONTEXT: Upstream).
Endpoint string
Model string
// Autonomy.
Mode Mode // Plan / Ask-Before / Allow-Edits / Auto (the privilege ladder)
Bypass bool // ADR 0006: Mechanisms off, structure on (the hard-constraint floor)
// ConfineToWorkspace tunes Auto's blast radius (ADR 0012); meaningful only in Auto.
// true (the default) fences subprocess writes to the workspace under OS confinement
// (network open, MCP gated); false ("I am the sandbox") runs Auto unconfined, safe
// only inside a VM. It is loaded from the GLOBAL config only (a project config cannot
// loosen it — the hostile-repo footgun is closed). The host sets it; the loop reads it
// in the dispatch disposition.
ConfineToWorkspace bool
// ConfineWritablePaths and ConfineNetworkAllow extend the confinement box beyond the
// workspace root (confinement-execution-contract §7): the toolchain cache/temp dirs a
// confined `go build`/`pip` needs to write, and the per-project network tightening
// list. The host probes/configures these and folds them into Config; the loop confines
// a subprocess to WorkspaceDir ∪ ConfineWritablePaths with ConfineNetworkAllow as the
// box's NetworkAllow. Empty NetworkAllow leaves the network open (the ADR 0012 default).
ConfineWritablePaths []string
ConfineNetworkAllow []string
// Host-supplied delegates. The host (TUI / bench / embedder) owns these.
Approver Approver // the human-in-the-loop gate; required unless Mode==Plan
Asker Asker // free-text Q&A delegate for the ask_user tool; nil ⇒ ask_user is not registered (P3.11)
Presenter Presenter // document-presentation delegate for the present_document tool; nil ⇒ present_document is not registered (ADR 0019)
Confiner Confiner // nil ⇒ no confinement ⇒ Auto is refused (ADR 0004)
Events EventSink // where typed Events are pushed; required
// Extension points. nil ⇒ the built-in defaults.
Tools *ToolRegistry // open extension point (ADR 0002)
// Mechanisms is the experimental-hook carrier: the bench registers candidate hooks on it via
// AddExperimental, and a host may pre-build catalogued Mechanisms into it directly. Catalogued
// Mechanisms are normally armed by ID through EnableMechanisms (ADR 0015), which builds each
// named Mechanism and merges it INTO this registry (a fresh one when nil), so the two coexist in
// one arm. The field keeps its name under v1 semver (no rename); EnableMechanisms is the
// enable-by-ID surface (ADR 0002/0003, ADR 0015).
Mechanisms *MechanismRegistry
// EnableMechanisms names catalogued Mechanisms to arm by ID (ADR 0015 §1). New and Resume build
// each named Mechanism at construction and merge it INTO Mechanisms (creating a fresh registry
// when that is nil), so a catalogued Mechanism and a bench experimental hook coexist in one arm.
// An unknown ID (ErrUnknownMechanism), an ID listed twice or already pre-built into Mechanisms
// (the registry's already-registered rejection), a hook-less Mechanism, or a half-armed Requires
// stack fails construction — a typo or a half-built stack never silently disables a Mechanism.
// Empty/nil arms nothing (the default-off posture). The catalogue's CONTENTS are data, not v1
// contract — an ID may change in a minor with a CHANGELOG notice; the field and its build
// semantics are the stable surface (locked decisions 1–2, 6).
EnableMechanisms []MechanismID
// Skills resolves the user's attached skill IDs (UserInput.SkillIDs) to their injectable
// bodies; nil ⇒ no skills are wired and any attached ID is reported and dropped. It is an
// interface defined here (not the concrete internal/skills catalog) so the loop fulfils the
// SkillIDs seam without domain importing skills — the dependency flows toward domain (ADR
// 0010). The host (cmd/apogee) loads the catalog and injects it.
Skills SkillResolver
// Injected state roots — no implicit ~/.apogee (ADR 0001). The bench points
// these at ephemeral dirs so sim runs never touch the production Library.
LibraryDir string
SessionsDir string
ConfigDir string
// WorkspaceDir is the sandbox root the built-in file tools are scoped to when
// Config.Tools is nil. Empty ⇒ no default tools are wired (the host must inject
// Config.Tools to give the Agent any tools). The bench points it at an ephemeral
// workspace so a file-edit task never escapes its sandbox (ADR 0001 isolation).
WorkspaceDir string
// ExternalEffects is the single injectable boundary for non-forkable effects
// (network, MCP). nil ⇒ live. The bench injects a deterministic stub for v1;
// record/replay slots in behind the same interface later (ADR 0008).
ExternalEffects ExternalEffects
// WebSearchEndpoint is the search backend the web_search tool sends a query to
// (P3.11). DEFAULT-ON: empty ⇒ the tool falls back to its built-in DuckDuckGo
// provider (no API key needed); the sentinel "off" disables it (a graceful "web
// search is disabled", never a crash). The host folds a configured endpoint in from
// config.yaml.
WebSearchEndpoint string
// Profile describes how the configured model speaks the wire (CONTEXT: Model profile) —
// its tool-call format and inline thinking-channel style — so the loop selects the matching
// tool-call parser and content-stripper at the parse seam. A ZERO Profile == native tool
// calls with no inline thinking == today's exact behaviour (the byte-identical anchor): a
// native profile selects no-op parsers, so the content path is unchanged. The host folds a
// configured profile in from config.yaml; an embedder sets it directly. It is declarative
// DATA translated to internal/processing's parsers at the boundary (ADR 0010) — not the
// parsers' own config types, which cannot move up the DAG since processing imports domain.
Profile ModelProfile
// Budget / Compaction knobs (context/) are structural and load-bearing — they
// run even under Bypass. Defaults are sane; overrides are advanced.
Context ContextConfig
}
Config is the full construction surface. It carries the Upstream target, the autonomy posture, the host-supplied delegates, the extension registries, and the injected state roots. A zero Config is not valid; Endpoint, Model, and Events are the minimum. A struct (not functional options) because every field is a deliberate, reviewable seam and ADR 0001 speaks of state "injected via Config".
type Confinement ¶
type Confinement struct {
Confiner Confiner
Box ConfinementBox
}
Confinement is the handle a subprocess tool uses to confine the *exec.Cmd it builds: the Confiner backend plus the box to confine to. The dispatch disposition installs it into the Execute context (WithConfinement) for a subprocess tool the Auto disposition chose to run confined; the tool retrieves it (ConfinementFromContext) and calls Confine on its cmd before running it. Keeping the handle on the context — rather than in domain.Tool.Execute's signature — preserves the open Tool extension point (ADR 0002) while giving the subprocess tools (P3.8) exactly the contract's tool-builds-and-runs- the-cmd model (confinement-execution-contract §2.2).
func ConfinementFromContext ¶
func ConfinementFromContext(ctx context.Context) (Confinement, bool)
ConfinementFromContext returns the Confinement handle installed by WithConfinement and whether one is present. ok is false when the call is not running under a confinement disposition (every mode other than Auto/confine-on for a subprocess tool), in which case the tool runs its command unconfined — the disposition has already ensured that path is only reached where unconfined execution is the intended outcome.
type ConfinementBox ¶
type ConfinementBox struct {
WorkspaceRoot string
WritablePaths []string
NetworkAllow []string // per-project tightening; empty = network open (ADR 0012)
}
ConfinementBox is the confinement policy for a run. Default = workspace-write-only + network OPEN + per-project allowlist (ADR 0012). NetworkAllow is a TIGHTENING list: empty leaves the network open; non-empty opts the box into network-deny.
type ConfinementCaps ¶
ConfinementCaps is the capability matrix a Confiner reports (ADR 0012 — extensible beyond these two).
func (ConfinementCaps) AutoEligible ¶
func (c ConfinementCaps) AutoEligible() bool
AutoEligible reports whether these capabilities satisfy the Auto gate. Under ADR 0012 the network is open by default, so Auto requires filesystem-write confinement ONLY (was FSWrite && NetworkEgress under ADR 0004). NetworkEgress is still reported and matters only when a user opts back into network-deny via box.NetworkAllow. A host without fs-confinement is no longer refused Auto — it gates the subprocess surface instead (confinement-execution-contract §5).
type Confiner ¶
type Confiner interface {
// Capabilities reports what this backend can actually enforce, here and now —
// probed once at construction, never optimistic (confinement-execution-contract
// §5). A kernel without landlock, or a macOS without sandbox-exec, reports
// {false, false}, so the dispatch disposition gates the subprocess surface rather
// than confining it.
Capabilities() ConfinementCaps
// Confine prepares cmd to execute confined to box, then RETURNS — it does not run
// cmd (confinement-execution-contract §2.2). It rewrites cmd to launch under the
// host OS confinement facility (macOS: exec under sandbox-exec -p <profile>; Linux:
// interpose the landlock re-exec wrapper) and sets cmd.SysProcAttr so the caller's
// process-group kill reaches the wrapped child. The caller has already wired
// Stdin/Stdout/Stderr/Dir/Env and afterwards invokes cmd.Run()/Output(). The PARENT
// process is never restricted.
//
// Confine is only invoked when Capabilities() reports box is enforceable on this
// host (the disposition checks caps first, §4). ErrConfinementUnavailable is the
// runtime safety net: a backend that finds it cannot establish the box returns it,
// and the caller falls back to Approval ("confine if you can, gate if you can't").
Confine(ctx context.Context, box ConfinementBox, cmd *exec.Cmd) error
}
Confiner is the OS-level confinement facility for the unbounded subprocess surface (ADR 0012). The interface is PUBLIC because the host injects it via Config; the backends (seatbelt / landlock / AppContainer) live in internal/platform.
Granularity is the single, all-OS subprocess (Linux landlock applied to the child after fork, before execve; macOS sandbox-exec wrapping the child). There is no in-process per-thread confinement — Apogee's own in-process writes are path-safety-bounded instead (ADR 0012's blast-radius split).
type ContextConfig ¶
type ContextConfig struct {
MaxContextTokens int // 0 ⇒ window unknown; the CLI discovers it or the context-window key supplies it (Budget/Compaction inactive until known)
ResponseReserve int
CompactionEnabled bool // generative summarisation; default true
}
ContextConfig governs the structural context reducers — Budget and Compaction — which are NOT Mechanisms and stay on under Bypass (CONTEXT: Budget, Compaction).
type Conversation ¶
type Conversation struct {
// contains filtered or unexported fields
}
Conversation is the serializable conversation state a history-rewrite hook edits. It is a cleanly copyable value with no live handles (ADR 0001) — what lets the bench fork by deep-copying it and the user resume from a snapshot. Summaries are not a separate structure: they are ordinary messages produced by generative Compaction (context/) and written back via Replace. A deferred Response Action (ActionDefer) is held here (Defer / TakeDeferred) so it survives a snapshot/resume boundary.
MarshalJSON / UnmarshalJSON keep the type opaque while persisting it; the v1 wire schema persists the message list (with per-message Extra preservation, P1.6) and the pending deferred corrections. The engine wraps this payload in its session-state envelope (internal/agent/state.go), which adds the loop counters.
func NewConversation ¶
func NewConversation(messages []Message) *Conversation
NewConversation builds a Conversation over a copy of messages (engine seam).
func (*Conversation) Append ¶
func (c *Conversation) Append(m Message)
Append adds m to the end of the history — the engine's per-Turn commit of a user, assistant, or tool-result message, and the natural primitive a history-rewrite hook uses to grow the conversation (a summary, a gap note). It is Insert at Len with a name that reads at the call site.
func (*Conversation) AssistantBoundaries ¶
func (c *Conversation) AssistantBoundaries() []int
AssistantBoundaries are the indices of assistant messages — the only safe cut points, because a tool result must stay adjacent to the assistant call that produced it (strict chat templates).
func (*Conversation) At ¶
func (c *Conversation) At(i int) Message
At returns the message at index i (panics on an out-of-range index, like a slice).
func (*Conversation) ClearDeferred ¶
func (c *Conversation) ClearDeferred()
ClearDeferred discards all pending deferred corrections. A Deferred Response Action is a decision about the NEXT request of the SAME conversation flow, so the loop clears the queue whenever an Exchange ends (completeTurn's Exchange-complete branch, abandonTurn, AbortExchange): a stale fan-out directive must never survive a fault or abort into the next Exchange (F6).
func (*Conversation) Defer ¶
func (c *Conversation) Defer(inject string)
Defer records a deferred correction (the Inject payload of an ActionDefer PostResponseDecision) to be injected, role-safe, into the next request. It is held in conversation state so it survives a snapshot/resume boundary — the streaming feed-forward path (design §4.1).
func (*Conversation) DeferredLen ¶
func (c *Conversation) DeferredLen() int
DeferredLen reports how many deferred corrections are currently queued — the loop reads it after draining a request to capture the floor a cancelled Turn's own deferrals are truncated back to (TruncateDeferred), so a re-attempt restores only the drained injections (F6).
func (*Conversation) DropRange ¶
func (c *Conversation) DropRange(start, end int)
DropRange drops messages in [start, end) — history truncation drops the middle, keeping the prefix and a recent tail. Bounds are clamped; an empty range is a no-op.
func (*Conversation) Insert ¶
func (c *Conversation) Insert(i int, m Message)
Insert places a message at index i — e.g. a static gap note at a truncation cut. i is clamped to [0, Len].
func (*Conversation) MarshalJSON ¶
func (c *Conversation) MarshalJSON() ([]byte, error)
MarshalJSON serializes the Conversation (messages + pending deferred corrections).
func (*Conversation) Messages ¶
func (c *Conversation) Messages() []Message
Messages returns a copy of the message list (engine seam — the loop projects it onto the provider wire shape).
func (*Conversation) PrefixEnd ¶
func (c *Conversation) PrefixEnd() int
PrefixEnd is the index past the leading system messages and the first user message — the protected prefix a truncation must keep.
func (*Conversation) Range ¶
func (c *Conversation) Range(fn func(i int, m Message) bool)
Range iterates messages until fn returns false.
func (*Conversation) Replace ¶
func (c *Conversation) Replace(msgs []Message)
Replace swaps the entire message list — generative Compaction writes its summarised history back through here. The slice is copied.
func (*Conversation) Revision ¶
func (c *Conversation) Revision() int
Revision reports how many mutations have been applied to the Conversation — the loop's acted-fire probe (R4, engine seam): hookrun snapshots it around each catalogued history-rewrite fire and books the fire only when the counter moved. A hook never needs it, and it does not survive a snapshot round-trip.
func (*Conversation) SetMessageContent ¶
func (c *Conversation) SetMessageContent(i int, content string)
SetMessageContent edits one message's content in place by index. An out-of-range index is a no-op.
func (*Conversation) TakeDeferred ¶
func (c *Conversation) TakeDeferred() (injects []string, ok bool)
TakeDeferred removes and returns the pending deferred corrections in FIFO order — the loop drains them when building the next request and InjectContexts each. ok is false when none are pending.
func (*Conversation) TruncateDeferred ¶
func (c *Conversation) TruncateDeferred(n int)
TruncateDeferred drops every deferred correction past the first n, keeping the queue's first n entries. n is clamped to [0, len]. The loop calls it when rolling a cancelled Turn back: the deferrals the cancelled Turn's own post-response hooks queued die with the Turn, so restoreDeferred re-queues the drained injections exactly once rather than atop a contradictory re-derivation (F6).
func (*Conversation) UnmarshalJSON ¶
func (c *Conversation) UnmarshalJSON(data []byte) error
UnmarshalJSON restores a Conversation from its serialized form.
type ConversationView ¶
type ConversationView interface {
Len() int
At(i int) Message
Range(fn func(i int, m Message) bool)
// LastUser returns the most recent user message and its index.
LastUser() (msg Message, index int, ok bool)
// CallByID resolves a tool result to its originating call (for the name/args).
CallByID(id string) (call ToolCall, index int, ok bool)
// ResultFor resolves a tool call to its result message.
ResultFor(callID string) (msg Message, index int, ok bool)
}
ConversationView is read-only history with the tool-call/result pairing helpers every history-inspecting Mechanism needs: the tool name and arguments live only on the originating ToolCall, never on the tool-result message, so resolving a result back to its call is mandatory for error-handling Mechanisms.
type ErrorEvent ¶
ErrorEvent reports a localised, recovered fault — a tool or Mechanism panic caught at the extension boundary, or a tool execution error (ADR 0007). It does not imply the loop stopped.
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is the sealed sum type of everything the loop reports. It is sealed (an unexported marker) so the variant set stays owned by Apogee and additively versioned; external code switches on the concrete types but cannot add variants.
type EventBase ¶
EventBase is embedded in every Event variant. Depth is the sub-agent nesting level (0 = top-level agent); a sub-agent's events nest into the parent's stream with Depth > 0 (ADR 0005). Turn is the Turn index the event belongs to.
It is exported so the engine and other internal subsystems can construct Event variants (setting Turn/Depth), but it is deliberately NOT re-exported by the root facade: the sealing method eventDepth() stays unexported in this package, so no package outside internal/* can satisfy Event — the variant set remains closed.
type EventSink ¶
type EventSink interface {
Emit(Event)
}
EventSink receives typed Events as the loop produces them, including *inside* a Step (streaming). The TUI adapts these to Bubble Tea messages; the bench consumes them as Go values. Emit must not block the loop for long — fan out if needed.
type ExchangeView ¶
type ExchangeView struct {
// contains filtered or unexported fields
}
ExchangeView is the current Exchange derived from a conversation read surface: the opening user message and the messages strictly after it. It is a working value — construct it where needed with CurrentExchange and let it go; it holds the backing reader, so derive it again after a mutation rather than keeping one across edits.
func CurrentExchange ¶
func CurrentExchange(c messageReader) ExchangeView
CurrentExchange derives the current Exchange from c: the opening is the last RoleUser message, the Exchange the messages strictly after it. With no user message present there is no current Exchange (Found reports false).
func (ExchangeView) After ¶
func (e ExchangeView) After() []Message
After returns copies of the messages strictly after the opening user message — the current Exchange's body. It returns nil when no user message exists or nothing follows the opening.
func (ExchangeView) Found ¶
func (e ExchangeView) Found() bool
Found reports whether an opening user message exists — without one there is no current Exchange.
func (ExchangeView) RangeAfter ¶
func (e ExchangeView) RangeAfter(fn func(i int, m Message) bool)
RangeAfter walks the messages strictly after the opening user message without allocating, calling fn with each message's index in the backing view, until fn returns false. It is a no-op when no user message exists.
func (ExchangeView) UserIndex ¶
func (e ExchangeView) UserIndex() int
UserIndex returns the index of the opening user message, or -1 when no user message exists.
type ExternalEffectKind ¶
type ExternalEffectKind string
ExternalEffectKind classifies a non-forkable external effect.
const ( EffectNetwork ExternalEffectKind = "network" EffectMCP ExternalEffectKind = "mcp" )
type ExternalEffectTool ¶
type ExternalEffectTool interface {
Tool
ExternalEffect() ExternalEffectKind
}
ExternalEffectTool is an optional interface a Tool implements when it reaches state Apogee does not own (network, MCP). The loop routes these through Config.ExternalEffects so the bench can stub them deterministically (ADR 0008), and the Confiner/Approval gate treats them as unconfinable in Auto (ADR 0004).
type ExternalEffects ¶
type ExternalEffects interface {
Do(ctx context.Context, call ToolCall) (ToolResult, error)
}
ExternalEffects is the single injectable boundary for non-forkable external effects (ADR 0008). Production uses a live implementation; the bench injects a deterministic stub (network-unreachable / empty-MCP) without touching tool code.
type FingerprintConfidence ¶
type FingerprintConfidence int
FingerprintConfidence tags how strongly a ModelFingerprint identifies the model behind the Upstream. The Library keys learned observations on a fingerprint and gates injection on this tier ("prefer not to inject under uncertainty", CONTEXT "Library"): a low-confidence identity is easily aliased — two different builds can advertise the same label — so an observation keyed there is the weakest evidence and an inject Mechanism may decline it.
const ( // ConfidenceLow is a metadata label: the model id the Upstream advertises. It is the // always-available fallback, and the weakest tier because two distinct builds can share // one label (CONTEXT: "keyed on the model name" was the predecessor's gap). ConfidenceLow FingerprintConfidence = iota // ConfidenceMedium is a behavioral-probe identity (`apogee probe`, Phase 5). The slot // exists so the store format and the FingerprintResolver seam are forward-compatible; // no resolver produces it yet — the probe is explicitly out of scope for Phase 4 (D8). ConfidenceMedium // ConfidenceHigh is a weights-hash: a digest derived from the reachable model file, so // two builds that share a label but differ in weights resolve to distinct fingerprints. ConfidenceHigh )
func (FingerprintConfidence) String ¶
func (c FingerprintConfidence) String() string
String renders the confidence tier for logging and diagnostics.
type FingerprintResolver ¶
type FingerprintResolver interface {
// Resolve returns the best-available fingerprint for modelID. A resolver that cannot
// identify the model returns the zero ModelFingerprint rather than an error — an
// unidentified model simply leaves the Library inert.
Resolve(modelID string) ModelFingerprint
}
FingerprintResolver resolves the model behind the Upstream to a confidence-tagged ModelFingerprint. It is the seam for the three identity tiers: a production resolver returns the best available (a weights-hash when the model file is reachable, else the metadata label), and the Phase-5 behavioral probe (ConfidenceMedium) slots in behind this same interface without the loop changing (D8). Domain declares the seam; internal/library implements it (ADR 0010 — the dependency points at domain).
type FinishReason ¶
type FinishReason string
FinishReason is the model's stop reason; the set is open (treat unknown values defensively).
const ( FinishStop FinishReason = "stop" FinishLength FinishReason = "length" FinishToolCalls FinishReason = "tool_calls" )
type HistoryRewriter ¶
type HistoryRewriter interface {
RewriteHistory(ctx context.Context, conv *Conversation) error
}
HistoryRewriter edits conversation state — the home of truncate_history. A capability that may attach at more than one point (CONTEXT: Hook point). The Conversation is itself the history, so this hook reads and mutates it directly.
type HookPoint ¶
type HookPoint string
HookPoint is where in the loop a Mechanism fires — the primary classification (CONTEXT: Hook point). The set is fixed by the loop's structure.
const ( HookPreRequest HookPoint = "pre-request" // shape the outgoing request HookPostResponse HookPoint = "post-response" // inspect response, choose an action HookPreToolExec HookPoint = "pre-tool-exec" // between decision-to-run and execution HookPostToolResult HookPoint = "post-tool-result" // act on a result before the model sees it HookHistoryRewrite HookPoint = "history-rewrite" // edit conversation state (may attach widely) )
type LoopView ¶
type LoopView interface {
Conversation() ConversationView
Tools() []ToolDef
Budget() Budget
Turn() int
// Depth reports the sub-agent nesting level the hook is firing at: 0 for a top-level
// Agent, parent+1 for a sub-agent (ADR 0013). It is the seam a gate keyed on "only at
// the top level" needs — guided decomposition steers only the primary call, never a
// nested delegation it itself set up (ADR 0014 §5). A view built without a depth (a test
// fake, the degraded no-view Response) reports 0, the top-level default.
Depth() int
// Fired reports how many times a Mechanism has ACTED this Session (R4): an
// invocation is booked only when it mutated its working value or returned a
// non-zero post-response Action — an inspect-and-do-nothing invocation is not a
// fire. It is the seam for cross-Mechanism coupling (e.g. decompose muting itself
// once a read-loop Mechanism has fired) without a shared mutable meta map. An
// experimental hook's synthetic ID keeps counting every invocation (bench
// observability).
Fired(id MechanismID) int
}
LoopView is the read-only window every hook has onto loop state beyond its own mutable value — the conversation so far, the tool menu, the budget, the Turn index, and a self-regulation query. It is the home of all cross-Turn reads: most Mechanisms decide by aggregating across Turns, so the primary mutable value (a *Response, *ToolCall, *ToolResult) is never sufficient alone. Request and Response expose it via their View method; the tool-stage hooks receive it as an argument.
type Mechanism ¶
type Mechanism interface {
Descriptor() MechanismDescriptor
Ordering() OrderingConstraints
}
Mechanism is a catalogued unit of gated, self-regulating behaviour (CONTEXT: Mechanism). It supplies a descriptor and ordering constraints, and implements at least one hook interface above; the registry type-asserts which. A hook without a descriptor is an experimental hook (no self-regulation — ADR 0002).
type MechanismDescriptor ¶
type MechanismDescriptor struct {
ID MechanismID
Capability Capability
Suppression SuppressionPolicy
// IncompatibleWith constrains stacking — Mechanisms that must not co-fire.
IncompatibleWith []MechanismID
// Requires constrains stacking the other way — Mechanisms that must all be
// registered for this one to be enabled (the dual of IncompatibleWith). It is the
// enable-time declaration that this Mechanism is benched as a stack with its named
// peers: enabling it without them is a startup error (ValidateRequirements, ADR 0014
// §4). Enable-time only — live suppression of a required peer mid-Session is not
// re-checked.
Requires []MechanismID
}
MechanismDescriptor is per-Mechanism metadata orthogonal to its hook point (CONTEXT: Mechanism descriptor). The single source of truth for what Bypass turns off (by Capability) and what may co-fire (IncompatibleWith).
type MechanismFiredEvent ¶
type MechanismFiredEvent struct {
EventBase
Mechanism MechanismID
Hook HookPoint
Action string // e.g. the PostResponseDecision taken, or "suppressed"
}
MechanismFiredEvent reports that a Mechanism (or experimental hook) fired at a hook point — the observability spine for self-regulation and bench attribution.
type MechanismID ¶
type MechanismID string
MechanismID is the canonical, stable identifier of a Mechanism — also the stable tiebreak in the deterministic total order (ADR 0003).
const ExperimentalMechanismID MechanismID = "experimental"
ExperimentalMechanismID is the synthetic MechanismID a descriptor-less experimental hook fires under (ADR 0002 — no descriptor, no self-regulation). It exists so MechanismFiredEvent.Mechanism is never empty for bench attribution, and it is RESERVED: Add refuses a catalogued Mechanism claiming it, so a real Mechanism can never masquerade as the bench's own instrument or inherit its always-booked fire accounting (R5, phase-4-review-fixes item 4).
type MechanismRegistry ¶
type MechanismRegistry struct {
// contains filtered or unexported fields
}
MechanismRegistry is the injectable catalogue plus the bench's experimental-hook slots (ADR 0002/0003). The built-in catalogue is curated; Add is how internal Mechanisms join, AddExperimental is how the bench registers a candidate hook.
func NewMechanismRegistry ¶
func NewMechanismRegistry() *MechanismRegistry
NewMechanismRegistry returns a registry seeded with the built-in catalogue. The Phase-0 catalogue is empty — the curated Mechanisms land with the catalogue→hook mapping session (Phase 4); P0.6 needs only the experimental-hook slots.
func (*MechanismRegistry) Add ¶
func (r *MechanismRegistry) Add(m Mechanism) error
Add registers a catalogued Mechanism. It returns an error if the Mechanism claims the reserved experimental sentinel ID, re-uses an already-registered MechanismID (topoSort's byID map would otherwise silently drop one of the two — a loud failure instead, phase-4-review-fixes item 5), or implements no hook interface. (The constraint-cycle check is performed by New over the whole graph — a startup gate, ADR 0003 — so a registry under construction can hold constraints that only close a cycle once every Mechanism is present.)
func (*MechanismRegistry) AddExperimental ¶
func (r *MechanismRegistry) AddExperimental(at HookPoint, hook any) error
AddExperimental registers a bench experimental hook at a hook point — a behaviour that is not (yet) a Mechanism (CONTEXT: Experimental hook). It runs but does not join self-regulation. hook must implement the interface for at.
func (*MechanismRegistry) Experimental ¶
func (r *MechanismRegistry) Experimental(at HookPoint) []any
Experimental returns the experimental hooks registered at hook point at, in registration order. It is the read seam the engine drives the loop through without reaching into the registry's unexported storage (ADR 0010 — internal subsystems see domain through its methods, the same way the public surface does).
func (*MechanismRegistry) Ordered ¶
func (r *MechanismRegistry) Ordered(at HookPoint) []Mechanism
Ordered returns the catalogued Mechanisms that hook at at, in the deterministic total order the loop dispatches them (ADR 0003 / D4): a topological sort of their Before/After constraints with a stable tiebreak by canonical MechanismID, so the order is independent of registration order. Only Mechanisms implementing the interface for at are returned; a constraint naming a Mechanism absent from at is ignored (ordering is relative to the co-located Mechanisms). It is the read seam the engine dispatches catalogued Mechanisms through, the counterpart to Experimental for the descriptor-carrying catalogue.
func (*MechanismRegistry) ValidateIncompatibilities ¶
func (r *MechanismRegistry) ValidateIncompatibilities() error
ValidateIncompatibilities reports ErrIncompatibleMechanisms if two registered Mechanisms declare each other incompatible (MechanismDescriptor.IncompatibleWith). It is the second construction-time gate alongside ValidateOrdering — a loud startup failure (ADR 0003), so a config enabling two mutually-exclusive Mechanisms is refused rather than silently running both. New calls it once the whole graph is present.
func (*MechanismRegistry) ValidateOrdering ¶
func (r *MechanismRegistry) ValidateOrdering() error
ValidateOrdering reports ErrOrderingCycle if the catalogued Mechanisms' Before/After constraints form a cycle (ADR 0003 — a constraint cycle is a startup error). New calls it once the whole graph is present.
func (*MechanismRegistry) ValidateRequirements ¶
func (r *MechanismRegistry) ValidateRequirements() error
ValidateRequirements reports ErrMissingRequirement if any registered Mechanism declares a required peer (MechanismDescriptor.Requires) that is not itself registered. It is the third construction-time gate alongside ValidateOrdering and ValidateIncompatibilities — the dual of the incompatibility check: where incompatibility refuses two Mechanisms that must never co-fire, this refuses a Mechanism enabled without a peer it is benched as a stack with, a loud startup failure (ADR 0003 posture, ADR 0014 §4). Enable-time only: live suppression of a required peer mid-Session is accepted and not re-checked. New calls it once the whole graph is present.
type Message ¶
type Message struct {
Role Role
Content string
ToolCalls []ToolCall // RoleAssistant only
ToolCallID string // RoleTool only — links the result to its ToolCall.ID
// contains filtered or unexported fields
}
Message is a read-only snapshot of one conversation message handed to hooks. A hook reads Messages and mutates by index against the owning container (Request / Conversation); it never holds the loop's backing storage.
func (Message) Extra ¶
func (m Message) Extra(key string) (json.RawMessage, bool)
Extra reports a preserved unknown wire field on the message (reasoning_content, tool_choice, thinking, …). Round-trip preservation of these is load-bearing for snapshot/resume and the bench's fork, so they survive a history rewrite.
func (Message) MarshalJSON ¶
MarshalJSON serializes the Message as its known wire fields with any preserved Extra fields flattened alongside them. Known fields win on a key collision, so a stale extra entry can never shadow a real field. A Message with no extras takes the fast path and marshals straight from messageJSON.
The preserved siblings are spliced on in sorted key order rather than via a map marshal, so the wire bytes are deterministic regardless of Go's map iteration order — snapshots containing reasoning_content (or any other Extra) are byte-reproducible, which a later snapshot diff/hash relies on.
func (*Message) UnmarshalJSON ¶
UnmarshalJSON restores a Message, decoding the known fields and collecting any unknown sibling fields into the preserved Extra set so they survive a snapshot round-trip.
func (Message) WithExtra ¶
func (m Message) WithExtra(key string, v json.RawMessage) Message
WithExtra returns a copy of m carrying an additional preserved wire field under key. The engine attaches the model's reasoning channel (reasoning_content) to a committed assistant message this way, so it survives snapshot/resume; an empty key or value is a no-op. It copies the extra set, so a caller already holding the original Message is unaffected.
type MessageEvent ¶
MessageEvent is a completed assistant message (the no-tool turn ends an Exchange).
type Mode ¶
type Mode string
Mode is the autonomy level governing whether tool calls need human approval (CONTEXT: Agent mode). It is orthogonal to Config.Bypass.
const ( // ModePlan is read-only: no writes, no command execution. ModePlan Mode = "plan" // ModeAskBefore requires an Approval for every write, command, and external reach // (a harmless read runs free). ModeAskBefore Mode = "ask-before" // ModeAllowEdits auto-approves Apogee's own workspace-scoped writes (path-safety- // bounded); shell/exec, network, MCP, third-party in-process tools, and any // out-of-workspace write still gate. It needs NO Confinement — path-safety bounds // the auto-approved writes and the human backstops the unbounded surface — so it is // identical on every OS (ADR 0012). ModeAllowEdits Mode = "allow-edits" // ModeAuto runs unbounded tool calls without per-call approval, tuned by // Config.ConfineToWorkspace (ADR 0012). With confinement on (the default), the // subprocess surface runs OS-confined to the workspace; an unfenceable tool (MCP) or // an out-of-workspace Apogee write still gates through Approval; if fs-confinement is // unavailable, the subprocess surface gates ("confine if you can, gate if you can't"). ModeAuto Mode = "auto" )
func NextMode ¶
NextMode returns the mode one rung up the privilege ladder, wrapping Auto back to Plan. An unknown or empty mode starts the cycle at Plan (the safest rung), so a caller can never get stuck off-ladder.
func TighterMode ¶
TighterMode returns the more restrictive of two autonomy modes — the one lower on the privilege ladder (Plan < Ask-Before < Allow-Edits < Auto). It is the sub-agent tighten-only helper (ADR 0013): a sub-agent's disposition takes the tighter of the parent's LIVE mode and the child's spawn mode, so a parent tightening mid-delegation (Shift+Tab down) reaches the still-running child, while a parent loosening can never loosen it. An off-ladder mode (empty/unknown) ranks with Ask-Before — the same safe default the dispatch disposition applies to an unrecognised mode — so a stray value can neither loosen nor over-tighten the result.
type ModelFingerprint ¶
type ModelFingerprint struct {
Label string
Confidence FingerprintConfidence
}
ModelFingerprint is the confidence-tagged identity the Library keys observations on (CONTEXT "Library"). Label is the resolved identity string — a weights-hash digest, a probe signature, or the bare metadata label — and Confidence records which tier produced it, because injection is gated on confidence. A zero ModelFingerprint (empty Label) means the model could not be identified; the Library treats it as inert (nothing to key on).
func (ModelFingerprint) IsZero ¶
func (f ModelFingerprint) IsZero() bool
IsZero reports whether the fingerprint failed to identify the model (no Label). An inert Library (nothing to observe or inject against) is the zero-fingerprint case.
type ModelProfile ¶
type ModelProfile struct {
// ToolCallFormat selects how the model emits tool calls. "" and FormatNative both mean the
// structured out-of-band tool_calls path (nothing to recover from visible content); a text
// format (FormatMarkdownFenced / FormatCustomRegex) is parsed from the model's visible
// content at the seam.
ToolCallFormat ToolCallFormat
// Pattern is the custom-regex tool-call pattern — mandatory for FormatCustomRegex, ignored
// for the other formats. Named capture groups name the tool and its arguments; the parser's
// own group/flag defaults apply at the boundary when its finer knobs are unset.
Pattern string
// Thinking selects the model's inline reasoning-channel style. A zero Thinking (ThinkingNone)
// leaves the Upstream-split reasoning_content path untouched (the default).
Thinking ThinkingProfile
}
ModelProfile describes how a given small model speaks the wire (CONTEXT: Model profile): two ORTHOGONAL axes — its tool-call format and its inline thinking-channel style (a model can emit native tool calls AND inline thinking; gpt-oss does both). It is declarative domain DATA on Config (host- or embedder-settable) that the loop translates to the internal/processing parsers at the parse seam, not the parsers' own config types — those cannot move up the dependency DAG because internal/processing imports domain (ADR 0010), and profile-as-data snapshots cleanly and seeds the deferred switchable-profile / `apogee probe` work. A ZERO ModelProfile == native tool calls, no inline thinking == today's exact behaviour (the byte-identical anchor).
type OrderingConstraints ¶
type OrderingConstraints struct {
Before []MechanismID
After []MechanismID
}
OrderingConstraints declares a Mechanism's position relative to others at its hook point (ADR 0003 — seeded from apogee-sim's type, now owned here). The loop builds a deterministic total order by topological sort with a stable tiebreak by MechanismID; a constraint cycle is a startup error (ErrOrderingCycle).
type PostResponseAction ¶
type PostResponseAction string
PostResponseAction enumerates the post-response decisions.
const ( ActionRetry PostResponseAction = "retry" // re-call the Upstream now ActionIntercept PostResponseAction = "intercept" // alter the response before the loop acts ActionDefer PostResponseAction = "defer" // schedule a correction into the next request )
type PostResponseDecision ¶
type PostResponseDecision struct {
Action PostResponseAction
// Inject is the correction text — injected into the retried request for
// ActionRetry, or the next request for ActionDefer (role-safe, like
// Request.InjectContext). Empty carries no correction (for ActionRetry, a bare
// re-stream).
Inject string
}
PostResponseDecision is the action a post-response Mechanism chooses (CONTEXT: Post-response decision). ActionIntercept is expressed by mutating the *Response in place (SetText / SetToolCallArguments) and carries no payload. ActionRetry re-calls the Upstream in the same Turn; a non-empty Inject makes it a correction retry — the loop re-streams the request with the superseded assistant message and the correction appended, request-scoped, never committed to history (R1, amending catalogue C5). ActionDefer carries the correction into the *next* request — the feed-forward path — held in conversation state as a Deferred Response Action so it survives a snapshot boundary.
type PostResponseHook ¶
type PostResponseHook interface {
PostResponse(ctx context.Context, resp *Response) (PostResponseDecision, error)
}
PostResponseHook inspects the model response (resp.View() for history and the tool menu) and chooses an action — mutating resp in place for ActionIntercept, or returning ActionRetry / ActionDefer.
type PostToolResultHook ¶
type PostToolResultHook interface {
PostToolResult(ctx context.Context, call ToolCall, result *ToolResult, view LoopView) error
}
PostToolResultHook acts on a tool result before the model next sees it — the home of correct_tool_result, new to the loop (the proxy could not host it). It receives the originating call (the tool name and arguments live there, not on the result) and the loop view (error handling often counts prior failures across Turns).
type PreRequestHook ¶
PreRequestHook shapes the outgoing request before it is sent. It reads the conversation, tool menu, and budget through req.View() and mutates the request in place (the request being built is the conversation as it will be sent).
type PreToolExecHook ¶
type PreToolExecHook interface {
PreToolExec(ctx context.Context, call *ToolCall, view LoopView) error
}
PreToolExecHook acts between the decision to run a tool and its execution. It receives the loop view because the decision is usually cross-Turn (e.g. short-circuiting a re-read of a file already read earlier needs the read history).
type PresentMethod ¶
type PresentMethod string
PresentMethod names the presentation-ladder rung that carried a document to the user. The tool result echoes it so the model can tell the user the truth ("opened on your machine" vs. "the path is shown in the transcript") rather than claiming an outcome it never saw. The set is open (additively extensible — treat unknown values defensively).
const ( // PresentOpened: the host opened the document on the user's own machine — the OS opener // (rung 1) or the configured present.command (rung 3). PresentOpened PresentMethod = "opened" // PresentServed: the document is registered with the doc server and its URL joined the // transcript entry (rung 2), for the user's terminal to linkify into the host's browser. PresentServed PresentMethod = "served" // PresentShown: the baseline alone (rung 0) — the workspace-relative path stands in the // transcript for the user to open. It is equally the outcome of a degraded higher rung, // which is why it is a normal result and not an error. PresentShown PresentMethod = "shown" )
type PresentOutcome ¶
type PresentOutcome struct {
// Method is the rung that ran — the highest one that succeeded, degraded to PresentShown
// when everything above the baseline failed or did not apply.
Method PresentMethod
// Location is where the user finds the document: the served URL for PresentServed, the
// DisplayPath otherwise.
Location string
}
PresentOutcome reports which rung of the presentation ladder actually carried the document to the user. A STRUCT for the same freeze-safety reason as PresentRequest, and the reason the tool never has to assert a success it cannot observe: it relays this outcome verbatim.
type PresentRequest ¶
type PresentRequest struct {
// Path is the ABSOLUTE path of the document to present. The tool has already resolved it
// inside the workspace root and confirmed it is an existing regular file, so a Presenter
// receives a path it may hand straight to a mechanism.
Path string
// DisplayPath is Path in its workspace-relative form — the text the transcript carries as
// plain text on its own line, for the terminal (Zed / VS Code / iTerm2 / WezTerm / kitty)
// to linkify. It is display-only: mechanisms use Path.
DisplayPath string
// Title is an optional human label for the document; it MAY be empty. The host renders it
// above the path when set.
Title string
}
PresentRequest is the document put in front of the user. It is a STRUCT (not a bare path) for freeze-safety (D7), the same reason AskRequest is one: a post-v1 field (a content-type hint, a "reveal in folder" flag) is then an additive, non-breaking change to the v1.0.0 surface.
type Presenter ¶
type Presenter interface {
Present(ctx context.Context, req PresentRequest) (PresentOutcome, error)
}
Presenter is the host-supplied delegate the present_document tool routes a finished deliverable to: the model names a document it has just written and the HOST decides how the user sees it (the presentation ladder — the transcript baseline always, the OS opener when the session is local and a desktop exists, a doc-server URL when it is remote, a user-configured command when one is set). The model supplies a path, never a mechanism.
It is the sibling of Asker (P3.11): the same host-decides delegate shape, for showing a document rather than asking a question. Like Asker it is NOT a safety gate — a Presenter carries no allow/deny semantics and never bypasses the Approval/disposition machinery — and it is MODE-INDEPENDENT: presenting writes nothing, so the tool is ReadOnly and runs in every mode, Plan included.
It is consulted synchronously inside a Step (on the worker goroutine) but, unlike Asker, it awaits no human rendezvous and must never block on the user. It must FAIL SAFE under cancellation: when ctx is cancelled it returns promptly rather than finishing a mechanism, so a cancelled Turn is never held open by a presentation.
A nil Presenter means the present_document tool is simply NOT REGISTERED (graceful), so a headless / non-interactive host never offers the model an affordance nobody can honour.
Fail visible, degrade to the baseline (ADR 0019): a mechanism that fails — no opener on this box, a doc server that cannot bind — is not an error, because the baseline rung has already put the path in front of the user. An implementation reports the rung it actually reached (PresentShown) instead; an error is reserved for a presentation that reached the user in no form at all.
type ReadOnlyTool ¶
ReadOnlyTool is an optional interface a Tool implements to declare that it performs no writes. It is the signal Plan mode filters on (only read-only tools run) and that Ask-Before uses to skip Approval for a harmless read. A Tool that does not implement it — or implements it returning false — is treated as write-capable, the safe default that gates. IsReadOnly is the helper the loop should call rather than the type assertion directly.
type ReasoningEvent ¶
ReasoningEvent is one newly-revealed chunk of the model's reasoning channel — the observability seam for "the model is thinking", which the visible TokenEvent stream by design never shows. It is emitted for BOTH reasoning paths: the provider's native channel (reasoning_content) and an inline <think>/harmony span held off the visible stream. Chunks arrive in order and concatenate to the reasoning the Turn's assistant message preserves; a Turn that reasons without emitting visible text produces ReasoningEvents and no TokenEvents. The concatenation is a liveness view, not a byte-exact copy: on the inline path a channel token split across deltas is revealed as span text while it accumulates, so the chunks can carry a partial closer (e.g. "secret</thi") that the completed token later removes from the preserved reasoning.
It is OBSERVATION ONLY: it never changes history or what the model receives. The reasoning channel is already preserved on the committed assistant message (reasoning_content), so an observer that ignores this event loses nothing but liveness. Arrival alone is a usable signal — a UI may render "thinking" from the event and never read Text at all.
Text is untrusted model output. Any consumer that DISPLAYS it must escape-strip it exactly as the TUI's token path (transcript.appendToken) does before it reaches a terminal; the raw chunk may carry ESC bytes.
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request is the outgoing Upstream request a pre-request hook may shape. Reads go through View; mutations are the characterised operation set from apogee-sim's pre-request Mechanisms. The loop builds one with NewRequest, hands it to every pre-request hook (their mutations compose), then drains it with State to project onto the provider wire shape.
func NewRequest ¶
func NewRequest(model string, messages []Message, tools []ToolDef, budget Budget, turn int, fired map[MechanismID]int) *Request
NewRequest builds the pre-request working value from loop state (engine seam). The messages and tools slices are copied, so a hook mutating the Request never reaches back into the loop's conversation storage. fired, in contrast, is shared BY REFERENCE: it is the loop's live per-Session fire ledger LoopView.Fired reads, so a Mechanism can see a peer's fire from earlier in the same hook pass (the decompose↔read_loop coupling seam). It is only ever read through the view — no view operation mutates it — so the shared reference is safe. nil is fine (Fired then reports 0 for every Mechanism).
func (*Request) AppendSupersededAssistant ¶
AppendSupersededAssistant appends a superseded assistant message (text + tool calls) to the end of the request — the loop's retry-exchange seam (engine seam, R1), NOT a hook-mutation primitive: on an ActionRetry correction the loop appends the response it is retrying, then the correction via InjectContext, so the re-streamed request carries the exchange the sim's retry builders carried. The append is request-scoped — it is never committed to history. A wholly empty superseded response (empty text, no calls) appends nothing. calls is copied, so the caller's slice stays independent.
The FIRST call freezes committedLen at the current length (item 10, sim parity): this superseded attempt, its correction, and every later accumulated retry are request-scoped and stay out of the post-response scanners' View(). It is frozen once — not advanced per retry — because the sim's detectors ran against the ORIGINAL committed request on every retry iteration, so the scanner view stays pinned to the pre-retry length throughout. The freeze precedes the empty-response short-circuit, so an empty superseded + a correction is bounded too.
func (*Request) AppendToSystem ¶
AppendToSystem appends text to the first system message (creating one if absent), but is a no-op if marker already occurs there — the idempotent inject the nudge Mechanisms (library, cot, decompose) share. Reports whether it injected. The caller embeds marker within text so a second call with the same marker is a no-op.
func (*Request) Extra ¶
func (r *Request) Extra(key string) (json.RawMessage, bool)
Extra reports a preserved unknown request field (e.g. a grammar Mechanism checks for an existing response_format before setting one).
func (*Request) InjectContext ¶
InjectContext inserts a user message at the role-safe position: appended to the system prompt if the conversation ends in a tool result (a user message after a tool result breaks strict chat templates); appended at the end if it ends in an assistant message (the retry-exchange shape — the correction answers the superseded assistant message it follows, R1); otherwise inserted before the last user message. With no user message present it appends at the end.
func (*Request) Revision ¶
Revision reports how many mutations have been applied to the Request — the loop's acted-fire probe (R4, engine seam): hookrun snapshots it around each catalogued fire and books the fire only when the counter moved. A hook never needs it.
func (*Request) SetDepth ¶
SetDepth records the sub-agent nesting level this request runs at (engine seam, ADR 0013/0014): the loop stamps it from Agent.depth so a pre-request hook reading req.View().Depth() can tell a top-level (0) from a nested request. It is loop setup, not a hook mutation — it carries no acted-fire meaning and so does NOT bump the revision. A Request built without it reports Depth 0, the top-level default.
func (*Request) SetExtra ¶
func (r *Request) SetExtra(key string, v json.RawMessage)
SetExtra sets an unknown request field, allocating the carrier if needed (e.g. a grammar constraint sets response_format).
func (*Request) SetMessageContent ¶
SetMessageContent edits one message's content in place by index — tool-result capping and history-collapse of older messages. An out-of-range index is a no-op.
func (*Request) SetSampling ¶
func (r *Request) SetSampling(p SamplingParams)
SetSampling overrides sampling parameters. Forward-looking — no current Mechanism mutates these; included so the surface need not change to add one.
func (*Request) SetTools ¶
SetTools replaces and reorders the tool menu (the tool-filter Mechanism). The slice is copied so the caller cannot mutate the menu after the call.
func (*Request) State ¶
func (r *Request) State() RequestState
State returns the Request's current state after any hook mutations (engine seam). The slices and the extras map are copies, so the loop's projection cannot disturb the Request and a later hook (none run after the drain today) would still see a faithful value.
func (*Request) View ¶
View exposes the read-only conversation/tools/budget window. The conversation is bounded to committedLen once a retry-in-place has appended a superseded exchange (item 10): the post-response scanners then see only committed history + the response under review, never the request-scoped superseded attempt/correction — matching the sim, whose retry builders ran their detectors against the unmutated request. The tool menu and budget are unbounded.
type RequestState ¶
type RequestState struct {
Model string
Messages []Message
Tools []ToolDef
Sampling SamplingParams
Extras map[string]json.RawMessage
}
RequestState is the post-hook state of a Request the loop reads to build the provider request (engine seam). Hooks shape the Request through its mutators and never call State.
type ResolvedSkill ¶
ResolvedSkill is one attached skill reduced to the fields the loop injects: the ID and DisplayName label the prepended block, and Body is the skill's instruction text scoped to the turn it was attached to.
type Response ¶
type Response struct {
// contains filtered or unexported fields
}
Response is the model response a post-response hook inspects and may intercept. The loop builds one with NewResponse from the parsed Upstream reply; reads go through the accessors, and ActionIntercept is expressed by mutating in place.
func NewResponse ¶
func NewResponse(text, thinking string, toolCalls []ToolCall, finish FinishReason, view LoopView) *Response
NewResponse builds the post-response working value from the parsed reply (engine seam). view is the read window onto the conversation+tools+budget the response was produced against; a nil view degrades to an empty one so View never returns nil.
func (*Response) AppendToolCall ¶
AppendToolCall appends a synthesized tool call to the response and bumps the revision — the intercept seam a post-response Mechanism uses to add a delegation the model did not itself emit (guided decomposition synthesizing the first sub_agent call from the model's enumeration, ADR 0014 §2). The appended call is indistinguishable from a model-emitted one downstream: the loop reads it back through ToolCalls(), records it on the committed assistant message, and dispatches it through the full per-call Resolution — the ADR 0013 recursion point for a sub_agent call. The caller owns the call's ID (the loop's synthesized-call style) and arguments; combined with a returned ActionDefer the appended call and the deferred correction both take effect (hookrun applies the in-place mutation, then routes the defer).
func (*Response) FinishReason ¶
func (r *Response) FinishReason() FinishReason
FinishReason is the model's stop reason.
func (*Response) Revision ¶
Revision reports how many mutations have been applied to the Response — the loop's acted-fire probe (R4, engine seam): hookrun snapshots it around each catalogued fire and books the fire only when the counter moved or a non-zero Action was returned. A hook never needs it.
func (*Response) SetText ¶
SetText replaces the assistant text — the intercept path (ActionIntercept).
func (*Response) SetToolCallArguments ¶
func (r *Response) SetToolCallArguments(index int, args json.RawMessage)
SetToolCallArguments rewrites one tool call's arguments in place — the auto-fix Mechanism writing back repaired/formatted content (ActionIntercept). An out-of-range index is a no-op.
func (*Response) Thinking ¶
Thinking is the harmony/thinking channel content when the model and parser expose it (ok == false when there is none).
type SamplingParams ¶
SamplingParams are the optional sampling overrides a pre-request hook may set; a nil field leaves the loop's value untouched.
type Session ¶
type Session struct {
Version int // schema version; Resume rejects an unknown future version
State json.RawMessage // opaque serialized conversation state
}
Session is the serializable, copyable conversation state — no live handles, no process globals (ADR 0001). Deep-copying it yields an independent branch (the bench's fork primitive); Encode/Decode persist it (the user's resume feature).
func DecodeSession ¶
DecodeSession deserializes a session, returning ErrSessionVersion if the schema version is newer than this build understands.
type SkillResolver ¶
type SkillResolver interface {
// ResolveSkills returns the resolved skills for ids, in the given order, skipping any
// unknown ID. The caller compares the result against what it requested to report a miss,
// so a typo in an attached ID is never silently swallowed.
ResolveSkills(ids []string) []ResolvedSkill
}
SkillResolver maps attached skill IDs to their injectable form. It is implemented by the skills catalog (internal/skills) and injected via Config.Skills; the interface lives in domain so the loop can fulfil the UserInput.SkillIDs seam without importing the skills package (ADR 0010 — the dependency flows toward domain).
type StepResult ¶
type StepResult struct {
Status StepStatus
TurnIndex int // 0-based index of the Turn just completed
Elapsed time.Duration // wall time for this Turn
}
StepResult reports the outcome of one Step at the quiescent boundary.
type StepStatus ¶
type StepStatus string
StepStatus is the disposition of a completed Step. The set is open (additively extensible — treat unknown values defensively).
const ( // StatusTurnComplete: the Turn finished and more Turns are pending (the model // requested tools; the loop will continue on the next Step). StatusTurnComplete StepStatus = "turn-complete" // StatusExchangeComplete: the model produced a final no-tool response; the // Agent now awaits the next Submit. StatusExchangeComplete StepStatus = "exchange-complete" // StatusCancelled: ctx was cancelled; state is serializable, resume is valid. StatusCancelled StepStatus = "cancelled" )
type StreamResetEvent ¶
type StreamResetEvent struct {
EventBase
}
StreamResetEvent signals that the assistant tokens streamed for the current Turn since the last boundary are superseded and must be discarded — the loop is re-streaming the Turn because an ActionRetry post-response decision re-called the Upstream. A streaming observer (the TUI) clears its in-progress token buffer for the Turn on this event; the MessageEvent that ends the Turn carries the final, accepted text.
type SubprocessTool ¶
type SubprocessTool interface {
Tool
// Subprocess reports that this tool launches an OS subprocess. It exists so a tool
// can implement the marker yet still report false (a degraded build), the safe
// default being treated as a non-subprocess tool.
Subprocess() bool
}
SubprocessTool is an optional interface a Tool implements to declare that it launches an OS subprocess (a shell, an interpreter, a child program) whose blast radius is the whole filesystem unless OS-confined — the unbounded surface ADR 0012 fences with the Confiner. The dispatch disposition keys on this marker to RUN such a tool inside Confiner.Confine in Auto with confine-to-workspace on (rather than gating it), and to gate it when fs-confinement is unavailable ("confine if you can, gate if you can't"). terminal / python-exec (P3.8) carry it; the in-process write tools do not (they are path-safety-bounded, not OS-confined). IsSubprocessTool is the helper the loop calls.
type SuppressionPolicy ¶
type SuppressionPolicy string
SuppressionPolicy is how a Mechanism participates in self-regulation (CONTEXT: Adaptive Suppression, Off-ramp). Exempt off-ramps still earn their place by their own leave-one-out A/B (ADR 0006 / ADR 0009) — exempt-from-suppression is not exempt-from-validation.
const ( SuppressStrikesThree SuppressionPolicy = "strikes-3" // suppressed after N non-helpful fires SuppressExempt SuppressionPolicy = "exempt" // never suppressed (off-ramps) )
type ThinkingProfile ¶
type ThinkingProfile struct {
// Style selects the stripping strategy: ThinkingNone (no inline channel, the default),
// ThinkingDelimited (a literal Start/End token pair), or ThinkingHarmony (gpt-oss channels,
// which need no tokens).
Style ThinkingStyle
// Start and End are the literal delimiter tokens for ThinkingDelimited (e.g. "<think>" /
// "</think>"); both must be set for stripping to run. They are ignored for the other styles.
Start string
End string
}
ThinkingProfile selects a model's inline thinking-channel style (CONTEXT: Thinking channel): the private reasoning the loop strips from visible content and preserves as reasoning in history. A zero ThinkingProfile (ThinkingNone) means no inline channel — content passes through untouched, the right default when the Upstream already splits reasoning into a separate reasoning_content field.
type ThinkingStyle ¶
type ThinkingStyle string
ThinkingStyle names a model's inline reasoning-channel format. "" is treated as ThinkingNone.
const ( // ThinkingNone is the default: no inline channel (the model emits none, or the Upstream // already split reasoning into reasoning_content). "" is treated the same. ThinkingNone ThinkingStyle = "none" // ThinkingDelimited is a literal Start/End token pair bracketing reasoning (e.g. // <think>…</think>). The exact tokens vary per model and even per build — the live smoke // test found gemma-4-e4b-it-qat emits <|channel>thought…<channel|> — so Start/End must be // set to what the model actually emits, not assumed from the model family. ThinkingDelimited ThinkingStyle = "delimited" // ThinkingHarmony is gpt-oss's harmony channel format (<|channel|>analysis<|message|>…). ThinkingHarmony ThinkingStyle = "harmony" )
type TokenEvent ¶
TokenEvent is one streamed chunk of assistant text. The tokens streamed for a Turn may be superseded by a StreamResetEvent (the loop re-streamed the Turn on an ActionRetry): accumulate TokenEvents per Turn and discard the accumulation when a reset arrives.
type Tool ¶
type Tool interface {
// Name is the stable identifier the model calls and the registry keys on.
Name() string
// Description and Schema are presented to the model (the JSON-schema of args).
Description() string
Schema() json.RawMessage
// Execute runs the call. It must honour ctx cancellation (ADR 0007) and the
// statelessness contract above. A panic here is caught at the loop's extension
// boundary and surfaced as an ErrorEvent.
Execute(ctx context.Context, call ToolCall) (ToolResult, error)
}
Tool is the public, open extension point: embedders may register their own.
Contract — stateless across Turns (ADR 0008): a tool's only durable side effect is filesystem writes; nothing live (process, REPL, socket, cursor) survives the quiescent boundary. terminal and python-exec are one-shot (fresh process per call). A tool needing persistence must serialize it into conversation state, not hold it live — this is what makes snapshot/resume and the bench's fork coherent.
type ToolCall ¶
type ToolCall struct {
ID string
Tool string
Arguments json.RawMessage
}
ToolCall is a parsed request from the model to run a tool.
type ToolCallEvent ¶
ToolCallEvent reports that the model requested a tool call (post-parse).
type ToolCallFormat ¶
type ToolCallFormat string
ToolCallFormat identifies how a model emits tool calls, so the loop can select the matching parser at the seam. Its values mirror internal/processing's ToolCallFormat so the boundary translation is a straight map. "" is treated as FormatNative.
const ( // FormatNative is the structured tool_calls path: calls arrive out-of-band and the text // parser finds nothing in the visible content ("" is treated the same). FormatNative ToolCallFormat = "native" // FormatMarkdownFenced is the markdown-fenced code-block tool-call format. FormatMarkdownFenced ToolCallFormat = "markdown-fenced" // FormatCustomRegex is the user-supplied named-group regex tool-call format (needs Pattern). FormatCustomRegex ToolCallFormat = "custom-regex" )
type ToolDef ¶
type ToolDef struct {
Name string
Description string
Schema json.RawMessage // JSON-schema of arguments
}
ToolDef is one entry of the tool menu the model sees.
type ToolRegistry ¶
type ToolRegistry struct {
// contains filtered or unexported fields
}
ToolRegistry is the injectable set of available tools (ADR 0001 — injectable, no globals). A sub-agent receives a subset of the parent's registry, never a superset (ADR 0005). Registration order is preserved so the tool menu the model sees is deterministic across runs (load-bearing for the bench's reproducibility).
func NewToolRegistry ¶
func NewToolRegistry() *ToolRegistry
NewToolRegistry returns an empty registry.
func (*ToolRegistry) All ¶
func (r *ToolRegistry) All() []Tool
All returns the registered tools in registration order — the read seam the loop builds the model's tool menu from without reaching into unexported storage.
func (*ToolRegistry) Lookup ¶
func (r *ToolRegistry) Lookup(name string) (Tool, bool)
Lookup returns the tool registered under name, and whether it was found — the seam the loop's dispatch resolves a parsed ToolCall against.
func (*ToolRegistry) Register ¶
func (r *ToolRegistry) Register(t Tool) error
Register adds a tool, returning ErrDuplicateTool on a name already present and ErrInvalidTool on an empty name (the model keys calls on the name, so it must be a stable, non-empty identifier).
func (*ToolRegistry) Subset ¶
func (r *ToolRegistry) Subset(names ...string) *ToolRegistry
Subset returns a new registry containing only the named tools, in the order named — the primitive a caller uses to narrow a sub-agent's tools (ADR 0005). Names not present in this registry are skipped, so the result can never be a superset of the parent; a repeated name is registered once.
type ToolResult ¶
ToolResult is what a tool returns to the loop (pre tool-result-capping).
type ToolResultEvent ¶
type ToolResultEvent struct {
EventBase
Result ToolResult
}
ToolResultEvent reports a tool's result after execution (and after any post-tool-result Mechanisms have acted on it).
type UsageEvent ¶
UsageEvent reports the token accounting an Upstream reply carried — the prompt (context) tokens, the generated completion tokens, and their total — once a Turn's stream reaches its terminal Done. A server that omits usage emits no UsageEvent, so an observer that never sees one simply has no token counts (the zero state). It is the observability spine for the live context-usage gauge and a tokens/sec readout: an observer reads the latest Depth-0 UsageEvent for the current context fill and times the completion against its own clock for throughput. Like every variant it nests by Depth, so a sub-agent's usage reaches the parent's observer at its nesting level.
type UserInput ¶
UserInput is one user message into an Exchange: free text plus optional file references the loop resolves into context, plus reserved skill references. Stays a value (no live handles) so it snapshots cleanly.
FileRefs (@file tokens parsed from the chat input) are resolved at Step time — the loop reads each within the workspace fence and prepends its content to the user message. SkillIDs are the skills the user attached in chat (the /skill command); the loop resolves each through Config.Skills and prepends its body to the user message for that one turn. The refs round-trip through a snapshot, so a resumed session re-resolves them.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package domaintest is the hook seam's shared test adapter (the internal/platform/confinetest precedent): conversation fixtures and a settable LoopView fake, so a Mechanism or engine test builds history and loop state through one vocabulary instead of hand-rolled per-file literals.
|
Package domaintest is the hook seam's shared test adapter (the internal/platform/confinetest precedent): conversation fixtures and a settable LoopView fake, so a Mechanism or engine test builds history and loop state through one vocabulary instead of hand-rolled per-file literals. |