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) Remaining() int
- func (b *Budget) SetMaxSteps(n int32)
- func (b *Budget) SoftCapExceeded() bool
- func (b *Budget) WithDeadline(parent context.Context) (context.Context, context.CancelFunc)
- type BudgetOptions
- type BudgetOwner
- type Event
- type InvocationContext
- type LLMResponse
- type LlmAgent
- type LlmAgentConfig
- type PauseOption
- type SwarmContextValue
- type ToolInvocation
- type TracerProvider
Constants ¶
const ( ToolInvocationStart = "start" ToolInvocationEnd = "end" )
ToolInvocationStart and ToolInvocationEnd label tool invocation lifecycle events.
const SystemPrompt = `` /* 11491-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"`
// 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) 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) 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 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 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
}
LlmAgentConfig carries the LlmAgent constructor inputs.
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.
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 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 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
- llm_agent.go
- llm_agent_args.go
- llm_agent_completion.go
- llm_agent_consume.go
- llm_agent_events.go
- llm_agent_finalize.go
- llm_agent_parallel.go
- llm_agent_pause.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 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). |