Documentation
¶
Overview ¶
Package agent owns the cornerstone runtime contract every later phase implements or consumes: the open Agent interface, the single-Run-scoped InvocationContext, the forward-compat Event/Actions/LLMResponse shape, the OTel-correct trace IDs, and the Budget tree that bounds a run.
The Agent interface is OPEN by design (no unexported seal): Phase 3 LlmAgent and Phase 9 swarm implement it directly. This diverges from google/adk-go, which seals its interface via an unexported internal() method so only its own constructors can implement it — adk's own docs note they are moving toward an open interface, which is the premise Aura locks in now (D-01).
Budget is the cornerstone resource-exhaustion control (a DoS-prevention mechanism, ASVS V11): a single shared *atomic.Int32 step counter bounds the WHOLE agent tree, a wallclock deadline bounds total time, and a two-phase dedup ring (budget_dedup.go) bounds repeated tool calls.
Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0). Adattato per Aura con SC#2 budget exhaustion + SC#3 child-inherits-remaining + SC#4 UUIDv7 OTel-compat.
The shared-atomic design (D-10) is what makes SC#3 true: a depth-3 fan-3 tree consumes ≤ max_steps total, NOT max_steps³ — it avoids the fresh-per-child explosion that nanobot-style designs fall into. Budget.Child shares the SAME counter by pointer and never forks it.
Two-tier loop-guard dedup (D-18/A2), split out of budget.go per RESEARCH #1 so neither file approaches the 600-LOC no-god-class cap (CLAUDE.md).
WHY two-tier and WHY result is VETO-only: the original (name,args,result)-in-hash design was FAIL-OPEN — any tool returning a volatile field (timestamp, page-token, request-id) never produces a repeating triple, so a runaway loop is never detected and slips past the guard. A2 reversed it: the primary fingerprint is sha256(name + canonical_json(args)) ONLY, checked in BeforeToolCall BEFORE the tool re-executes (so repeated side effects are blocked). A bounded result preview is recorded later by AfterToolResult and used ONLY as a progress VETO: if the args repeat but the latest result preview CHANGED, the next BeforeToolCall suppresses dedup and treats it as progress. Volatile-result tools therefore fail SAFE (look like progress) instead of fail-open.
CALLER-CANONICALIZES CONTRACT (B2): BeforeToolCall and AfterToolResult both accept PRE-CANONICALIZED argsCanonicalJSON []byte. The CALLER runs internal/canonicaljson.Marshal(args) before calling — these methods do NOT call canonicaljson internally. Plan 05 Task 2 (the LoopAgent caller) and this callee agree on this contract; keeping canonicalization in the caller lets the loop hash once per turn and avoids a hidden re-serialization.
LlmAgent is Aura's first real Agent implementation (D-01 commit 6): the budget-gated tool-dispatch run-loop that drives the openai_compat client, threads ToolResult into in-memory history, streams chunk/tool-call/tool-result/ final Events, and terminates via text_response with a content-stop fallback. It resolves the Phase-2 deferred SpanID minting (full-tree crypto/rand, tracing.go) and emits one llm.request span per LLM call.
The loop shape replicates agenttest.CountingAgent.Run (the canonical budget-gated iter.Seq2 emitter): budget gate BEFORE each call -> on a trip yield a terminal Event (never the error slot, D-04); the yield-after-false guard is mandatory.
Completion critic gate (amendment #54 / D-43): the spine that turns the prompt's "verify before reporting" from prose into an enforced contract. Before the loop accepts a VOLUNTARY termination (text_response or the content-stop fallback) on a turn that mutated host state, a cheap critic call judges the user's request against the OBSERVED tool results — not the agent's claims — and vetoes ONCE when the promised deliverable is not verifiably present. It reuses the maybeRecover counter discipline (D-08): a dedicated completionAttempts counter (max 1) keeps the veto bounded, the extra turn rides the normal budget gate, and a broken/empty/unparseable critic fails OPEN so a verifier outage can never wedge a turn. Concern-split out of llm_agent.go to keep that file under the no-god-class cap.
consume is the stream-drain half of the run loop, split out of llm_agent.go to keep that file under the no-god-class cap (D-07). It turns one provider stream into the (text, calls, finish, usage) the loop acts on, re-emitting chunk/ tool-call/reasoning Events as they arrive and honoring the iter.Seq2 yield-after-false contract (a consumer break drains-to-close, then reports stopped so Run never yields again).
Forced finalization (Req#2, the SPINE for plan 04's recovery/fallback work): when the run-loop trips an early-termination gate (budget max_steps/wallclock, or the dedup veto) it used to die on a prose-less terminalBudgetEvent — the ~1-in-6 empty answer. finalize() instead issues ONE tool-free synthesis turn (ToolChoice="none", full in-memory history + the D-03 nudge), drains it to a content string, and emits a NON-EMPTY finalEvent that still carries the trip reason in StateDelta for observability (landmine #10).
Concern-split out of llm_agent.go (D-07): that file is at its no-god-class headroom, and the recovery counter + Italian-stub fallback land here in plan 04.
Pause-DETECTION seam (D-A1-03 / AM-01). This file holds ONLY the half of the HITL pause primitive that lives in the agent: catching the tools.ErrAwaitingUserInput sentinel, applying intra-turn exclusivity, and emitting the pause as an Actions.AwaitingInput Event. The agent stays DB-free — pause PERSISTENCE + resume orchestration live in the Runner (04-05), which observes this Event and is the sole writer of aura.paused_states. Split out of llm_agent.go for separation of concern (AM-01), not size.
The pause is Event-only, NEVER the iter.Seq2 error slot (RESEARCH Pattern-3 anti-pattern; mirrors budget exhaustion at llm_agent.go terminalBudgetEvent).
Output-budget truncation handling, split out of llm_agent.go to keep that file under the no-god-class cap. When a tool-call turn finishes with finish_reason= "length" its arguments were cut mid-JSON by the output budget, so dispatching it yields "unexpected end of JSON input" and the model retries the same oversized call — the 203-turn truncation thrash observed live 2026-06-14. Tool turns get the full output budget (reasoning_policy floor) so this fires only on a genuinely oversized call; when it does, nudge once and finalize on a repeat so the loop can never thrash.
Package agent's OTel wiring (D-03..D-06): the real TracerProvider bootstrap, full-tree crypto/rand SpanID minting (resolving the Phase-2 deferral at agent.go:51-52), and the per-call llm.request span helper. This file REPLACES the otel_deps.go blank-import anchor — the v1.44.0 trace train is now used for real, so the anchor is deleted in the same commit (truth: go.mod keeps the four modules pinned because tracing.go imports them).
Index ¶
- Constants
- Variables
- func AgentOwnsBudget(a Agent) bool
- func ExemptToolsFromEnv(extra ...string) map[string]struct{}
- func WithSwarmContext(ctx context.Context, budget *Budget, registry *tools.Registry, ...) context.Context
- type Actions
- type Agent
- type AwaitingInput
- type Budget
- func (b *Budget) AfterToolResult(name string, argsCanonicalJSON, resultPreview []byte)
- func (b *Budget) BeforeToolCall(name string, argsCanonicalJSON []byte) (dedup bool, reason string)
- func (b *Budget) BranchConsumed() int
- func (b *Budget) Child(fanout int) *Budget
- func (b *Budget) ConsumeStep() (ok bool, reason string)
- func (b *Budget) NodeTimeout() time.Duration
- func (b *Budget) Now() time.Time
- func (b *Budget) Release(n int)
- func (b *Budget) Remaining() int
- func (b *Budget) SetMaxSteps(n int32)
- func (b *Budget) SoftCapExceeded() bool
- func (b *Budget) TryReserve(n int) bool
- func (b *Budget) WithDeadline(parent context.Context) (context.Context, context.CancelFunc)
- type BudgetOptions
- type BudgetOwner
- type CommandHook
- func (h *CommandHook) AfterTool(ctx context.Context, call llm.ToolCall, res tools.ToolResult) (*ToolResultHookResult, error)
- func (h *CommandHook) BeforeModel(ctx context.Context, req *llm.Request) (*ModelHookResult, error)
- func (h *CommandHook) BeforeTool(ctx context.Context, call llm.ToolCall) (*ToolHookResult, error)
- func (h *CommandHook) OnTurnEnd(ctx context.Context, turn HookTurn) error
- func (h *CommandHook) OnTurnStart(ctx context.Context, turn HookTurn) error
- type CommandHookConfig
- type CommandHookDecision
- type CommandHookEnvConfig
- type CommandHookEvent
- type Event
- type FailPolicy
- type Hook
- type HookManager
- func (m *HookManager) AfterTool(ctx context.Context, call llm.ToolCall, res tools.ToolResult) (*ToolResultHookResult, error)
- func (m *HookManager) BeforeModel(ctx context.Context, req *llm.Request) (*ModelHookResult, error)
- func (m *HookManager) BeforeTool(ctx context.Context, call llm.ToolCall) (*ToolHookResult, error)
- func (m *HookManager) OnTurnEnd(ctx context.Context, turn HookTurn) error
- func (m *HookManager) OnTurnStart(ctx context.Context, turn HookTurn) error
- func (m *HookManager) Register(h Hook)
- func (m *HookManager) RegisterWithPolicy(h Hook, policy FailPolicy)
- type HookTurn
- type InvocationContext
- type LLMResponse
- type LlmAgent
- type LlmAgentConfig
- type ModelHookResult
- type PauseOption
- type SwarmContextValue
- type ToolHookResult
- type ToolInvocation
- type ToolResultHookResult
- type TracerProvider
Constants ¶
const ( ToolInvocationStart = "start" ToolInvocationEnd = "end" )
ToolInvocationStart and ToolInvocationEnd label tool invocation lifecycle events.
const EnvCommandHooks = "AURA_AGENT_COMMAND_HOOKS"
EnvCommandHooks carries an optional JSON array of command-hook configs. It is intentionally default-off: unset/blank means no command hooks are registered.
const SystemPrompt = `` /* 12051-byte string literal not displayed */
SystemPrompt is Aura's canonical system prompt — the operator-authored XML-tagged rewrite (2026-06-06, commit 6cf1895e) with the <skills> section aligned to amendment #51/#52 (the action=catalog/action=install routing those amendments deleted is gone; discovery+install is the always-on find-skills skill riding the host terminal). It is a package constant — never templated, never carrying a timestamp or any per-turn-mutating value — so it stays byte-identical across turns and preserves OpenRouter's implicit prompt-cache discount (Req#14; memory: reference_aura_cache_poisoning_sites). It explains MECHANISMS (the agentic loop, tool_search discovery, the skill capability-gap doctrine, the shell_exec full-terminal home, ask_user approvals, the Phase-15 <memory> doctrine — D-01 agent-decides writes, D-03 pull-on-demand recall) WITHOUT enumerating the volatile tool set — enumeration would cache-bust the prefix every time the tool set changes; concrete tool schemas ride in req.Tools OUTSIDE this prefix. Only the structural verbs (tool_search, text_response, skill, ask_user, shell_exec) are named. Authored in English with an explicit output-language directive (memory: feedback_all_prompts_in_english_only: never mix IT/EN in the prompt itself — drive output language via a directive). This Go constant is the authored source of truth; do not recreate stale prompt copies in docs.
Variables ¶
var ErrBudgetExhausted = errors.New("agent budget exhausted")
ErrBudgetExhausted is the exported sentinel for callers that inspect agent termination OUTSIDE the Event stream (D-04). Inside a Run the canonical signal is an explicit Event (Actions.Escalate=true + StateDelta termination_reason); this sentinel exists only so Phase 3/9 consumers can do errors.Is(err, agent.ErrBudgetExhausted) when a budget limit surfaces through the error slot.
Functions ¶
func AgentOwnsBudget ¶
AgentOwnsBudget reports whether an agent has opted into owning its own budget gates. Workflow parents use this to keep the single-budget-owner contract for a composed agent tree.
func ExemptToolsFromEnv ¶
ExemptToolsFromEnv returns the AURA_LOOP_DEDUP_EXEMPT_TOOLS allowlist plus any extra tool names, as a fresh set (D-19). It lets a caller compose the operator's env exemptions with its own (e.g. the dry-run tool) and pass the result to NewBudget via BudgetOptions.ExemptTools — no process-global env mutation (WR-04).
func WithSwarmContext ¶
func WithSwarmContext(ctx context.Context, budget *Budget, registry *tools.Registry, client llm.Client, llmCfg llm.Config, convID string) context.Context
WithSwarmContext returns a ctx carrying the parent deps the swarm_spawn runner adapter reads. The agent's runTool calls this before dispatching each tool so a swarm_spawn call can resolve the live parent budget/registry/client/config. It mirrors tools.WithToolCallContext.
Types ¶
type Actions ¶
type Actions struct {
Escalate bool `json:"escalate,omitempty"` // true → stop this branch / cancel siblings
StateDelta map[string]any `json:"state_delta,omitempty"` // termination_reason/limit_hit/steps_consumed, etc.
ArtifactDelta map[string]any `json:"artifact_delta,omitempty"` // produced-artifact deltas (forward-compat)
AwaitingInput *AwaitingInput `json:"awaiting_input,omitempty"` // HITL pause payload (Slice 1.5); nil unless ask_user fired
ToolInvocation *ToolInvocation `json:"tool_invocation,omitempty"`
// Display is the Phase-26 typed-display payload the display normalizer produced
// for a recognized tool result (HARDEN-08). It is an additive omitempty pointer
// mirroring AwaitingInput: a nil Display byte-omits the `display` key so the
// existing event wire is unchanged (decode(encode)==identity), and the AG-UI
// translator fans it out as an aura.display CUSTOM event when set.
Display *display.Payload `json:"display,omitempty"`
// DiscardStreamed is the mid-stream-retry repudiation signal (B-12). When a
// stream fails mid-way and the loop retries, the partial chunk Events the failed
// attempt already streamed are stale; the agent emits ONE Event carrying this
// flag BEFORE the retry's fresh chunks so a streaming consumer (the REPL renderer,
// the AG-UI gateway) drops what it has shown so far and renders the retry over a
// blank slate — instead of the user seeing partial+answer. It fires ONLY on a
// retry, so live token-by-token streaming on the common no-retry path is
// untouched (no buffering). Omitted on the wire unless set.
DiscardStreamed bool `json:"discard_streamed,omitempty"`
}
Actions are the control signals an Event carries. Escalate is the canonical termination/cancellation signal (D-04: budget exhaustion is Event-only, never the error slot). StateDelta/ArtifactDelta accept nested map[string]any and map to the AG-UI STATE_DELTA stream in Phase 12. AwaitingInput is the HITL pause signal (D-A1-03): a sibling to Escalate, set by the pause-detection seam (llm_agent_pause.go) when ask_user fires — the pause travels as this Event, NEVER the iter.Seq2 error slot. It is a pointer so an unset pause omits the `awaiting_input` key on the wire (mirrors the MessageID *pointer omitempty trick), keeping decode(encode())==identity.
type Agent ¶
type Agent interface {
// Name is the stable identifier used as the Event Author for this agent's
// runtime-emitted events and as the lookup key for FindAgent.
Name() string
// Description is the human/LLM-facing one-liner (Phase 3 surfaces it to the model).
Description() string
// Run executes one invocation and streams Events. The error slot of the
// iter.Seq2 carries only REAL failures (LLM/tool errors); termination and
// budget exhaustion are signalled by an explicit Event, never the error slot
// (D-04). Agents that consume Budget internally should implement BudgetOwner so
// workflow parents do not double-charge their emitted events.
Run(InvocationContext) iter.Seq2[*Event, error]
// SubAgents returns the direct children (nil for leaf agents).
SubAgents() []Agent
// FindAgent returns self if name matches, else recurses into SubAgents;
// nil if not found (collapses adk's FindAgent/FindSubAgent split).
FindAgent(name string) Agent
}
Agent is the cornerstone contract every later phase implements or consumes. Open by design (no unexported seal) — Phase 3 LlmAgent and Phase 9 swarm implement this directly. Pattern derivato da google/adk-go v1.4.0 agent/agent.go (Apache 2.0); the internal() seal is intentionally removed (D-01).
type AwaitingInput ¶
type AwaitingInput struct {
Question string `json:"question"`
Options []PauseOption `json:"options,omitempty"`
Kind string `json:"kind"`
Priority int `json:"priority,omitempty"`
ToolCallID string `json:"tool_call_id"`
ResumeContext json.RawMessage `json:"resume_context,omitempty"`
OriginAgent string `json:"origin_agent,omitempty"` // emitting agent name (swarm proxy forward-compat, D-A1-08)
ProxiedFromChildID string `json:"proxied_from_child_id,omitempty"` // child id when this pause relays a child's needs_user_input report (D-05); empty on a direct call
ProxiedToolCallID string `json:"proxied_tool_call_id,omitempty"` // child's originating tool_call id for the relay (D-05); empty on a direct call
}
AwaitingInput is the pause payload an ask_user Event carries (D-A1-03). It is the Event-side projection of tools.ErrAwaitingUserInput plus the dispatch- stamped ToolCallID and the originating-agent id for swarm forward-compat (D-A1-08: paused_states.proxied_* stay NULL for direct calls, populated by Phase 9 as the Event crosses the child→parent boundary). It carries no DB type — the askuser.Store reads these plain fields off the Event, never the other way round (the agent stays DB-free).
type Budget ¶
type Budget struct {
// contains filtered or unexported fields
}
Budget bounds one agent run. The steps counter is shared by pointer across the whole tree (D-10); deadlineWallclock and now are shared by value; the dedup ring is per-branch (forked by Child, D-09).
func NewBudget ¶
func NewBudget(opts BudgetOptions) (*Budget, error)
NewBudget builds a Budget applying CLI > env > builtin-default precedence (D-06) in ONE place: each opts override wins over the AURA_LOOP_* env value, which wins over the builtin default. It is fail-fast on any SET-but-malformed env value — it never silently falls back to a default on a parse error (the opposite of internal/config's silent-absorb int helper). Unset env vars use the builtin defaults; only a SET-but-malformed value is an error. No process-global state is mutated.
func NewBudgetFromEnv ¶
NewBudgetFromEnv builds a Budget from the AURA_LOOP_* environment with no overrides — the env-only callers' entry point. Fail-fast on any malformed value (D-06); see NewBudget for the precedence rules.
func (*Budget) AfterToolResult ¶
AfterToolResult records the bounded result preview for the fingerprint as a PROGRESS VETO (D-18). The caller passes the same already-canonical args (B2). If the result preview for a repeated fingerprint CHANGED since last time, the veto is set (progress) so the next BeforeToolCall suppresses dedup; if it is unchanged, the stale marker stays so period-1/period-2 repeat can terminate. resultPreview is truncated to the budget's resultCap before hashing (A7).
func (*Budget) BeforeToolCall ¶
BeforeToolCall is the PRE-EXECUTION dedup gate (D-18). The caller passes the already-canonical args bytes (B2). It returns (true, "dedup") when the same fingerprint has repeated to the window threshold (period-1) OR forms a period-2 ping-pong AND no progress veto applies — i.e. the loop should terminate before the side effect re-runs. Exempt tools (AURA_LOOP_DEDUP_EXEMPT_TOOLS, D-19) never dedup. The fingerprint is recorded so the next call can detect the repeat.
func (*Budget) BranchConsumed ¶
BranchConsumed is the number of steps THIS branch has consumed (the per-branch counter ConsumeStep bumps) — the <budget> "used" count (Req#6, D-06). There is no MaxSteps() getter (landmine #11): the agent reads this counter directly rather than deriving used from a cap.
func (*Budget) Child ¶
Child forks a budget for a PARALLEL branch (D-09): it shares the SAME *atomic.Int32 step counter and the same wallclock + clock (so the hard total bound is preserved across the tree), but gets a DISTINCT dedup ring (no cross-branch false positives) and a PASSIVE per-branch soft cap (D-12). Sequential/Loop sub-agents instead use InvocationContext.WithSubAgent, which shares the ring for cross-iteration repeat detection.
IN-04 — the soft cap is a SPAWN-TIME SNAPSHOT and therefore TIMING-DEPENDENT BY DESIGN: it is computed off b.Remaining() at the instant Child is called. When a ParallelAgent spawns siblings in a loop, a later sibling reads an already-depleted pool and so gets a SMALLER advisory share than an earlier one. This is intentional and consistent with the non-terminal "passive advisory" framing (D-12) — the soft cap never bounds correctness, only fairness hints — so siblings are NOT guaranteed equal shares. A caller that wants equal sibling shares must snapshot Remaining() once before the fan-out loop and reuse it across every Child call.
func (*Budget) ConsumeStep ¶
ConsumeStep is the TOCTOU-safe (D-11) gate every agent step passes through. It checks the wallclock FIRST (D-13), then does an atomic decrement-then-check-then-restore so N concurrent goroutines can never over-spend the shared cap: the goroutines that overshoot below zero all add 1 back. Only HARD terminal reasons are returned ("max_steps" | "wallclock") — the per-branch soft cap is NON-terminal and surfaced via SoftCapExceeded (D-12).
func (*Budget) NodeTimeout ¶
NodeTimeout is the optional per-node soft timeout (AURA_LOOP_NODE_TIMEOUT_SEC, D-13); zero means disabled.
func (*Budget) Now ¶
Now returns the budget's clock value. It is the runtime's single per-turn time source so prompt hints and wallclock gates agree in tests and production.
func (*Budget) Release ¶ added in v1.0.0
Release returns n previously-reserved steps to the shared pool (AG-038). It is the inverse of TryReserve; n<=0 is a no-op. Releasing more than was reserved would inflate the pool, so callers must Release exactly what they reserved.
func (*Budget) Remaining ¶
Remaining is the current shared step balance (never negative once restored).
func (*Budget) SetMaxSteps ¶
SetMaxSteps overrides the shared step counter (CLI flag precedence, D-06). It resets the WHOLE tree's remaining budget; intended only at boot before any child is spawned.
func (*Budget) SoftCapExceeded ¶
SoftCapExceeded reports whether THIS branch has consumed at least its passive fair-share soft cap (D-12). It is a non-terminal scheduling/fairness signal: the hard total bound (steps) stays authoritative, the branch may still consume against the shared pool. A root budget (branchSoftCap==0) never reports true.
func (*Budget) TryReserve ¶ added in v1.0.0
TryReserve atomically removes n steps from the shared pool, returning true on success. It is the TOCTOU-safe form of a Remaining() check (AG-038): a swarm parent reserves its post-fan-out synthesis budget BEFORE spawning children so a concurrent consumer cannot race the reservation away. On failure the pool is left unchanged (every overshooting reservation undoes its own decrement, like ConsumeStep). n<=0 is a no-op success. Pair every successful TryReserve with a Release once the reserved work is done (or abandoned).
func (*Budget) WithDeadline ¶
WithDeadline derives a context bounded by the budget's wallclock deadline so in-flight LLM/tool calls are cancelled end-to-end, not just blocked from new steps (D-13). The caller owns the returned CancelFunc.
type BudgetOptions ¶
type BudgetOptions struct {
MaxSteps *int // overrides AURA_LOOP_MAX_STEPS
MaxWallclockSec *int // overrides AURA_LOOP_MAX_WALLCLOCK_SEC
DedupWindow *int // overrides AURA_LOOP_DEDUP_WINDOW
ExemptTools map[string]struct{} // overrides AURA_LOOP_DEDUP_EXEMPT_TOOLS allowlist
// Now is the injectable clock (W8). When nil it defaults to time.Now. It is
// the SINGLE time source for BOTH the wallclock deadline anchor (computed as
// Now().Add(wallclock) at construction) AND the ConsumeStep deadline check, so
// a caller can drive wallclock behavior end-to-end through the constructor
// instead of building a Budget literally to bypass it (WR-03).
Now func() time.Time
}
BudgetOptions carries explicit overrides for the CLI > env > default precedence (D-06) WITHOUT touching process-global environment. A nil pointer field means "unset → fall through to env then builtin default"; a non-nil field overrides both. ExemptTools, when non-nil, replaces the env allowlist entirely (the caller has already merged in any operator-set exemptions). This lets `aura agent dry-run` pass resolved flag values directly instead of round-tripping through os.Setenv (WR-04), which was not goroutine-safe and collided with t.Setenv in parallel tests.
type BudgetOwner ¶
type BudgetOwner interface {
OwnsBudget() bool
}
BudgetOwner is an optional Agent capability: agents that already enforce their own Budget gates expose it so workflow parents can remain observational instead of double-charging the same shared budget.
type CommandHook ¶ added in v1.0.0
type CommandHook struct {
// contains filtered or unexported fields
}
CommandHook implements Hook by executing a trusted local command per event.
func NewCommandHook ¶ added in v1.0.0
func NewCommandHook(cfg CommandHookConfig) (*CommandHook, error)
NewCommandHook constructs a command hook. Execution is still gated by a fresh executable hash check on every event.
func (*CommandHook) AfterTool ¶ added in v1.0.0
func (h *CommandHook) AfterTool(ctx context.Context, call llm.ToolCall, res tools.ToolResult) (*ToolResultHookResult, error)
AfterTool sends an after_tool event to the command hook.
func (*CommandHook) BeforeModel ¶ added in v1.0.0
func (h *CommandHook) BeforeModel(ctx context.Context, req *llm.Request) (*ModelHookResult, error)
BeforeModel sends a before_model event to the command hook.
func (*CommandHook) BeforeTool ¶ added in v1.0.0
func (h *CommandHook) BeforeTool(ctx context.Context, call llm.ToolCall) (*ToolHookResult, error)
BeforeTool sends a before_tool event to the command hook.
func (*CommandHook) OnTurnEnd ¶ added in v1.0.0
func (h *CommandHook) OnTurnEnd(ctx context.Context, turn HookTurn) error
OnTurnEnd sends a turn_end event to the command hook.
func (*CommandHook) OnTurnStart ¶ added in v1.0.0
func (h *CommandHook) OnTurnStart(ctx context.Context, turn HookTurn) error
OnTurnStart sends a turn_start event to the command hook.
type CommandHookConfig ¶ added in v1.0.0
type CommandHookConfig struct {
Name string
Command string
Args []string
Env []string
ExpectedSHA256 string
Timeout time.Duration
}
CommandHookConfig describes a trust-gated out-of-process hook.
type CommandHookDecision ¶ added in v1.0.0
type CommandHookDecision struct {
Decision string `json:"decision"`
Message string `json:"message,omitempty"`
Content string `json:"content,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Request *llm.Request `json:"request,omitempty"`
ToolCall *llm.ToolCall `json:"tool_call,omitempty"`
ToolResult *tools.ToolResult `json:"tool_result,omitempty"`
}
CommandHookDecision is the JSON response read from a command hook's stdout.
type CommandHookEnvConfig ¶ added in v1.0.0
type CommandHookEnvConfig struct {
Name string `json:"name"`
Command string `json:"command"`
Args []string `json:"args"`
Env []string `json:"env"`
ExpectedSHA256 string `json:"expected_sha256"`
TimeoutMS int `json:"timeout_ms"`
FailPolicy string `json:"fail_policy"`
}
CommandHookEnvConfig is the operator-facing JSON shape accepted by EnvCommandHooks. TimeoutMS avoids time.Duration's nanosecond JSON footgun.
type CommandHookEvent ¶ added in v1.0.0
type CommandHookEvent struct {
Event string `json:"event"`
Turn *HookTurn `json:"turn,omitempty"`
Request *llm.Request `json:"request,omitempty"`
ToolCall *llm.ToolCall `json:"tool_call,omitempty"`
ToolResult *tools.ToolResult `json:"tool_result,omitempty"`
}
CommandHookEvent is the JSON envelope sent to a command hook on stdin.
type Event ¶
type Event struct {
RequestID uuid.UUID `json:"request_id"` // UUIDv7 TraceID/run_id, shared tree-wide
SpanID [8]byte `json:"span_id"` // 8-byte OTel SpanID, per-node; wire = lower-hex
ParentSpanID *[8]byte `json:"parent_span_id,omitempty"` // nil at root; wire = lower-hex when set
Author string `json:"author"` // workflow agent name, "user", or LLM agent name (D-14)
Branch string `json:"branch,omitempty"` // hierarchical label only (D-15)
ThreadID string `json:"thread_id,omitempty"` // AG-UI conversation thread (Phase 4 / Slice 1.8), forward-compat
MessageID uuid.UUID `json:"message_id,omitempty"` // AG-UI message correlation (UUIDv7); omitted on wire until set
LLMResponse *LLMResponse `json:"llm_response,omitempty"` // nil when this Event is not an LLM turn
Actions Actions `json:"actions"` // control signals (escalate, state/artifact deltas)
Timestamp time.Time `json:"timestamp"` // emit time, serialized RFC3339Nano UTC
}
Event is the single type every runtime- and LLM-emitted signal flows through. The shape is full / forward-compat so the Phase-12 AG-UI gateway is a fan-out adapter, not a refactor (D-17): MessageID/ThreadID/ToolCallID are present now even though nothing consumes them yet.
Trace IDs are OTel/W3C-correct widths (D-16): RequestID is a 16-byte UUIDv7 (TraceID/run_id), SpanID is 8 random bytes, ParentSpanID is nil at the root. Storing a 16-byte UUID in the SpanID slot would force lossy truncation when a future OTel slice maps these — 8 bytes makes that mapping drop-in.
WR-04 — SPAN MINTING IS DEFERRED, BY DESIGN, to the future OTel-integration slice (SPEC §"OTel dep transitive": Phase 2 ships the OTel-compatible SHAPE without the OTel dep; populating real per-node spans is out of Phase-2 scope). Phase 2 therefore leaves SpanID at its zero value [8]byte{} on every Event, which serializes as the constant "span_id":"0000000000000000". This all-zero value is the documented not-yet-minted sentinel, NOT a bug: the wire field exists now so the OTel slice fans out an existing field rather than retrofitting one. When that slice lands it mints a crypto/rand SpanID per child InvocationContext and chains ParentSpanID.
The struct stores SpanID/ParentSpanID as fixed-size byte arrays, but the wire form (see eventWire) is the OTel/W3C-idiomatic LOWER-HEX string, NOT the JSON number array a [N]byte array would default to. MessageID is value-typed here but omitted on the wire until set (eventWire uses *uuid.UUID so omitempty fires).
func (Event) MarshalJSON ¶
MarshalJSON is the SINGLE user-facing serialization path for an Event (W7): the Plan-07 dry-run prints Events through json.NewEncoder(...).SetEscapeHTML(false) honoring this method. canonicaljson is for hashing/fingerprinting only, never for Event output. HTML escaping is disabled and Timestamp is normalized to RFC3339Nano UTC so decode(encode(ev)) round-trips byte-identical (D-21).
func (*Event) SetAuthorIfEmpty ¶
SetAuthorIfEmpty stamps the author on an LLM-emitted Event that has not set one (D-14). Aura's open interface has no base-struct embed hook, so each workflow agent sets Author explicitly; this helper covers events minted deeper down.
func (*Event) UnmarshalJSON ¶
UnmarshalJSON decodes through eventWire (UseNumber for any nested StateDelta numbers, RFC3339Nano timestamp) so decode(encode(ev)) is symmetric.
type FailPolicy ¶ added in v1.0.0
type FailPolicy int
FailPolicy controls how a hook fault (error, timeout, or panic) is handled. FailOpen contains the fault — log + metric + allow the turn to proceed — while FailClosed aborts the turn with a clear reason. Security-gating hooks should register FailClosed; everything else defaults to FailOpen at the command-hook constructor. Bare NewHookManager/Register keep FailClosed so the historical turn-fatal contract for in-process hooks is unchanged.
const ( // FailClosed aborts the turn when a hook faults (default for Register). FailClosed FailPolicy = iota // FailOpen contains a hook fault and allows the turn to continue. FailOpen )
func (FailPolicy) String ¶ added in v1.0.0
func (p FailPolicy) String() string
type Hook ¶ added in v1.0.0
type Hook interface {
OnTurnStart(context.Context, HookTurn) error
BeforeModel(context.Context, *llm.Request) (*ModelHookResult, error)
BeforeTool(context.Context, llm.ToolCall) (*ToolHookResult, error)
AfterTool(context.Context, llm.ToolCall, tools.ToolResult) (*ToolResultHookResult, error)
OnTurnEnd(context.Context, HookTurn) error
}
Hook is the in-process plugin surface for one LlmAgent turn.
type HookManager ¶ added in v1.0.0
type HookManager struct {
// contains filtered or unexported fields
}
HookManager composes hooks in registration order. For result-producing hook points, the first non-nil result wins. Each hook carries a FailPolicy that governs how a fault (error/timeout/panic) at any point is handled.
func CommandHookManagerFromEnv ¶ added in v1.0.0
func CommandHookManagerFromEnv(lookup func(string) (string, bool)) (*HookManager, error)
CommandHookManagerFromEnv builds the optional out-of-process hook manager from EnvCommandHooks. A nil lookup uses os.LookupEnv; tests pass a narrow lookup.
func NewHookManager ¶ added in v1.0.0
func NewHookManager(hooks ...Hook) *HookManager
NewHookManager builds a manager, skipping nil hooks. Hooks default to FailClosed (turn-fatal on fault) — the historical contract.
func NewHookManagerWithPolicy ¶ added in v1.0.0
func NewHookManagerWithPolicy(policy FailPolicy, hooks ...Hook) *HookManager
NewHookManagerWithPolicy builds a manager whose hooks all share policy.
func (*HookManager) AfterTool ¶ added in v1.0.0
func (m *HookManager) AfterTool(ctx context.Context, call llm.ToolCall, res tools.ToolResult) (*ToolResultHookResult, error)
AfterTool gives registered hooks a chance to rewrite a completed tool result.
func (*HookManager) BeforeModel ¶ added in v1.0.0
func (m *HookManager) BeforeModel(ctx context.Context, req *llm.Request) (*ModelHookResult, error)
BeforeModel gives registered hooks a chance to rewrite or answer a model request.
func (*HookManager) BeforeTool ¶ added in v1.0.0
func (m *HookManager) BeforeTool(ctx context.Context, call llm.ToolCall) (*ToolHookResult, error)
BeforeTool gives registered hooks a chance to rewrite or veto a tool call.
func (*HookManager) OnTurnEnd ¶ added in v1.0.0
func (m *HookManager) OnTurnEnd(ctx context.Context, turn HookTurn) error
OnTurnEnd runs registered end hooks in order.
func (*HookManager) OnTurnStart ¶ added in v1.0.0
func (m *HookManager) OnTurnStart(ctx context.Context, turn HookTurn) error
OnTurnStart runs registered start hooks in order.
func (*HookManager) Register ¶ added in v1.0.0
func (m *HookManager) Register(h Hook)
Register appends h when non-nil with the default FailClosed policy.
func (*HookManager) RegisterWithPolicy ¶ added in v1.0.0
func (m *HookManager) RegisterWithPolicy(h Hook, policy FailPolicy)
RegisterWithPolicy appends h when non-nil with an explicit failure policy.
type HookTurn ¶ added in v1.0.0
type HookTurn struct {
AgentName string
ThreadID string
Branch string
RequestID string
Reason string
}
HookTurn carries immutable turn identity into lifecycle hooks.
type InvocationContext ¶
type InvocationContext struct {
Ctx context.Context // request-scoped cancellation/deadline (named, never embedded)
Agent Agent // the agent currently executing (self-reference for workflow recursion)
RequestID uuid.UUID // UUIDv7, 16 bytes — OTel/W3C TraceID + run_id shape (D-16), minted once at root
SpanID [8]byte // 8-byte OTel/W3C SpanID shape (D-16), per-node; minting DEFERRED to the OTel slice (WR-04) — stays [8]byte{} in Phase 2
ParentSpanID *[8]byte // nil at root; the parent node's SpanID otherwise — chaining DEFERRED to the OTel slice (WR-04)
Branch string // hierarchical LABEL only ("root.iter-2.worker-3"); hierarchy comes from span IDs (D-15)
Budget *Budget // shared budget tree (same *atomic.Int32 across the whole tree, D-10)
}
InvocationContext is single-Run-scoped; never store on a long-lived struct, never cache, never share across invocations. It is passed by value down the agent tree; WithContext/WithSubAgent always return a COPY and never mutate the receiver (D-24). Ctx is a NAMED field, never embedded — embedding invites passing the InvocationContext where a context.Context is expected and storing it, which the single-Run-scope invariant forbids.
func (InvocationContext) WithContext ¶
func (ic InvocationContext) WithContext(ctx context.Context) InvocationContext
WithContext returns a copy of ic with a replaced Ctx. The receiver is never mutated (D-24).
func (InvocationContext) WithSubAgent ¶
func (ic InvocationContext) WithSubAgent(sub Agent) InvocationContext
WithSubAgent returns a copy of ic re-pointed at sub, keeping the SAME *Budget (same *atomic.Int32 step counter AND the same dedup ring) so a single logical reasoning thread sees its own cross-iteration repeats (D-09). The receiver is never mutated (D-24). Parallel branches use Budget.Child instead to fork a distinct dedup ring while still sharing the atomic counter.
type LLMResponse ¶
type LLMResponse struct {
Content string `json:"content,omitempty"` // assistant text
Reasoning string `json:"reasoning,omitempty"` // stream-only CoT delta (amendment #57); never persisted to conversation_turns, additive D-17 forward-compat
ToolCalls []llm.ToolCall `json:"tool_calls,omitempty"` // reuses llm.ToolCall (D-17); its ID is the AG-UI tool_call_id
FinishReason string `json:"finish_reason,omitempty"` // provider finish reason on the final turn
}
LLMResponse carries the model output for an Event that IS an LLM turn. It is referenced via a pointer on Event so non-LLM events omit it entirely.
type LlmAgent ¶
type LlmAgent struct {
// contains filtered or unexported fields
}
LlmAgent drives the autonomous tool-dispatch loop. history is in-memory only this phase (D-26); messages[0] is the byte-stable system prompt (Req#14) and is never mutated. sessionID = Event.ThreadID (D-26) — the sidecar/spillover key.
func NewLlmAgent ¶
func NewLlmAgent(cfg LlmAgentConfig) *LlmAgent
NewLlmAgent builds an LlmAgent. messages[0] is ALWAYS the byte-stable system prompt (D-08/D-09) followed by the supplied user turns; the agent owns history from here. Name/Description default when empty.
func (*LlmAgent) Description ¶
Description is the human/LLM-facing one-liner.
func (*LlmAgent) OwnsBudget ¶
OwnsBudget tells workflow parents that LlmAgent already consumes the shared Budget at its own loop gates, so wrappers must not charge its emitted tool-call events a second time.
func (*LlmAgent) Run ¶
Run drives the budget-gated tool-dispatch loop (Req#9/#10). Termination paths: text_response (D-13), content-stop fallback on finish_reason=stop with no tool call (D-13/D-16), a budget trip (terminal Event with reason, D-04), or a real infra failure (the iter.Seq2 error slot, D-15).
type LlmAgentConfig ¶
type LlmAgentConfig struct {
Name string
Description string
Client llm.Client
LLM llm.Config
Registry *tools.Registry
PreviewCap int // AURA_CONTEXT_PREVIEW_CAP_BYTES (config.ToolPreviewCap)
RunDir string // config.RunDir — sidecar root
SessionID string // Event.ThreadID; sidecar dir key (D-26)
Workspace string // the shell workspace path, rendered into the per-turn tail hint (#52/D-41); "" omits
UserTurns []llm.Message
// Classifier is the SHARED long-lived reasoning-tier classifier (anchors built
// once, reused across turns). Production injects this via the Runner so the
// 18-seed anchor build + Neo4j example load is amortized, not paid per turn.
Classifier *prompt.ReasoningClassifier
// Embedder/ExampleStore are a convenience for tests/standalone construction:
// when Classifier is nil and Embedder is set, NewLlmAgent builds a per-agent
// classifier. Production leaves these unset and passes Classifier instead.
Embedder prompt.Embedder
ExampleStore prompt.ExampleStore
// Breaker is the SHARED process-lifetime circuit breaker (B-05). The Runner owns
// ONE breaker and injects it into every per-turn agent so a provider outage trips
// cross-turn protection (a fresh per-agent breaker reset each turn and never
// opened). nil => NewLlmAgent mints a fresh per-agent breaker (tests/standalone).
Breaker *llm.Breaker
// HookManager is the optional Phase-21 extension surface. nil is a no-op.
HookManager *HookManager
}
LlmAgentConfig carries the LlmAgent constructor inputs.
type ModelHookResult ¶ added in v1.0.0
type ModelHookResult struct {
Content string
FinishReason string
ToolCalls []llm.ToolCall
Usage llm.Usage
}
ModelHookResult lets a hook short-circuit a model request.
type PauseOption ¶
PauseOption mirrors a tools.Option on the wire. It is redeclared here (rather than importing tools.Option into the Event model) so internal/agent/event.go stays free of any tools dependency cycle; the pause seam copies the fields.
type SwarmContextValue ¶
type SwarmContextValue struct {
Budget *Budget
Registry *tools.Registry
Client llm.Client
LLMCfg llm.Config
ConvID string
}
SwarmContextValue carries the parent deps the internal/swarm RunnerAdapter needs to build a swarm RunConfig: the shared Budget tree, the parent tool Registry (the adapter derives the worker registry via Without(reg, "swarm_spawn")), the LLM Client + Config the workers run against, and the conversation id that keys each worker SessionID and the transcript dir. config.Config (RunDir, swarm caps) is NOT carried here — the adapter holds it as a construction-time field, so this package never imports internal/config.
CONCURRENT-READ CONTRACT (AG-062): every field is shared READ-ONLY across the fanned-out swarm workers. The *Registry is immutable post-boot (the agent registry is built once and never mutated during a run), so concurrent Get/All reads are safe without locking. The Budget's shared *atomic.Int32 step counter is the ONLY mutable cross-worker state and is atomic by construction (Child forks a distinct dedup ring per worker — no shared map). The llm.Client must be safe for concurrent Stream calls (the production OpenRouter client is). No worker writes any of these fields; a worker that needs per-call state derives it (Budget.Child, Without(registry,...)) rather than mutating the shared value. The `-race` swarm fan-out test (runner_adapter_test / swarm_test) guards this.
func SwarmContext ¶
func SwarmContext(ctx context.Context) (SwarmContextValue, bool)
SwarmContext reads the parent deps the runner adapter needs off the ctx. ok is false when the key is absent (a non-swarm dispatch path), letting the adapter return a model-readable error instead of panicking.
type ToolHookResult ¶ added in v1.0.0
type ToolHookResult struct {
Call *llm.ToolCall
Result *tools.ToolResult
}
ToolHookResult lets a hook rewrite or veto a tool call. A non-nil Result skips execution and is threaded as the tool result.
type ToolInvocation ¶
type ToolInvocation struct {
Event string `json:"event"`
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
Arguments string `json:"arguments,omitempty"`
ArgsBytes int `json:"args_bytes,omitempty"`
BatchIndex int `json:"batch_index,omitempty"`
BatchSize int `json:"batch_size,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
DurationMS int64 `json:"duration_ms,omitempty"`
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
ResultPreview string `json:"result_preview,omitempty"`
PreviewBytes int `json:"preview_bytes,omitempty"`
ResultBytes int `json:"result_bytes,omitempty"`
ResultTruncated bool `json:"result_truncated,omitempty"`
ResultSidecarPath string `json:"result_sidecar_path,omitempty"`
ExitCode *int `json:"exit_code,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
ToolInvocation is the typed audit payload emitted around real tool execution. It is intentionally runtime-only shape, not a DB type: the Runner projects it into the append-only tool invocation ledger.
type ToolResultHookResult ¶ added in v1.0.0
type ToolResultHookResult struct {
Result *tools.ToolResult
}
ToolResultHookResult lets a hook rewrite the result before it enters history.
type TracerProvider ¶
type TracerProvider interface {
// Shutdown flushes any batched spans and releases the exporter. The caller
// defers it on REPL exit; a bounded ctx keeps a missing collector from hanging.
Shutdown(ctx context.Context) error
}
TracerProvider is the package-edge handle the REPL (cmd/aura chat) and Phase-9 swarm hold to flush the span batch on exit (Req#13). It hides the otel SDK type so callers at the binary edge never import go.opentelemetry.io directly.
func NewTracerProvider ¶
func NewTracerProvider(ctx context.Context, mode, endpoint string) (TracerProvider, error)
NewTracerProvider is the exported wiring the binary edge uses to install the real exporter from AURA_OTEL_EXPORTER (D-05/D-06) and obtain a Shutdown handle. It delegates to newTracerProvider (the package-internal builder the agent loop's spans resolve against via the global provider) and returns the SDK provider as the narrow TracerProvider interface.
Source Files
¶
- agent.go
- budget.go
- budget_dedup.go
- errors.go
- event.go
- hooks.go
- hooks_command.go
- llm_agent.go
- llm_agent_args.go
- llm_agent_completion.go
- llm_agent_construct.go
- llm_agent_consume.go
- llm_agent_dispatch.go
- llm_agent_display.go
- llm_agent_events.go
- llm_agent_finalize.go
- llm_agent_parallel.go
- llm_agent_pause.go
- llm_agent_prefix.go
- llm_agent_reasoning.go
- llm_agent_retry.go
- llm_agent_stream_retry.go
- llm_agent_truncation.go
- metrics.go
- prompt.go
- swarm_context.go
- tracing.go
- trust.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agenttest holds the shared Agent mocks (D-07) that the workflow tests (Plan 05 Sequential/Loop, Plan 06 Parallel), the CLI dry-run (Plan 07), and future Phase 3/9 all reuse — one source of truth, zero inline mock duplication (CLAUDE.md "reusable code").
|
Package agenttest holds the shared Agent mocks (D-07) that the workflow tests (Plan 05 Sequential/Loop, Plan 06 Parallel), the CLI dry-run (Plan 07), and future Phase 3/9 all reuse — one source of truth, zero inline mock duplication (CLAUDE.md "reusable code"). |
|
Package display is the trust boundary for Phase-26's typed-display protocol (GAP-1 / HARDEN-08).
|
Package display is the trust boundary for Phase-26's typed-display protocol (GAP-1 / HARDEN-08). |
|
Package mcptools bridges a generic MCP server's tools into Aura's agent tool registry: it lists the server's tools and adapts each to a tools.Tool whose Execute routes through the MCP client's tools/call.
|
Package mcptools bridges a generic MCP server's tools into Aura's agent tool registry: it lists the server's tools and adapts each to a tools.Tool whose Execute routes through the MCP client's tools/call. |
|
Package prompt owns the single wire-Request assembly chokepoint (PromptBuilder) and the content fingerprint (PrefixHash) that the cache-invariant gate reads.
|
Package prompt owns the single wire-Request assembly chokepoint (PromptBuilder) and the content fingerprint (PrefixHash) that the cache-invariant gate reads. |
|
Package tools defines the tool interface the agent loop dispatches against, the deferred-tool flag that keeps big specs out of the default LLM manifest, and the built-in `tool_search` hook the model uses to fetch deferred specs.
|
Package tools defines the tool interface the agent loop dispatches against, the deferred-tool flag that keeps big specs out of the default LLM manifest, and the built-in `tool_search` hook the model uses to fetch deferred specs. |
|
Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0).
|
Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0). |