Skip to content
Spice Framework on GitHub

Architecture

agentMaturity: experimentalSource: spice-agent@4726fb8Exact reviewed source

Governing boundary

For compiled code, the Spice bean graph is the extension graph. Constructors receive exact typed dependencies; ordered slices and named maps are generated by Spice; primary, fallback, qualifiers, lifecycle, modules, and test overrides use Spice directly. agent.Engine executes an already-constructed graph and is not a container.

The kernel owns provider-neutral values, one deterministic single-agent loop, sequenced events, cancellation, panic containment, and snapshots. It does not own providers, filesystem or shell tools, persistence, permissions, telemetry, MCP, Git, indexing, TUI code, subagents, or distributed scheduling.

New and resumed execution use a transactional kernel boundary. PrepareStart allocates the stable run ID and acquires the current immutable plan; snapshot preparation acquires the exact recorded generation. Both validate and build an unregistered event log without publishing events or starting execution. An outer host can therefore acquire authority before Commit atomically registers the run and starts it with a separately supplied caller-owned root context. Abort/Close release the uncommitted log and lease exactly once. Preparation contexts never become run lifetimes, and no protocol or daemon authority policy is embedded in the kernel.

Runtime plugins are the sole dynamic graph. A run leases one immutable plugin generation; activation never changes existing runs and never alters compiled DI. The kernel expresses this boundary generically as stage.ToolPlanSource and stage.ToolPlanLease; it has no plugin process or protocol knowledge. Existing embedded constructors use StaticToolPlanSource, preserving direct-call Go. Ordered dispatcher decorators are applied once to a merged dispatcher and must preserve its snapshotted definition set. A leased dispatcher rejects any call not present in that snapshot.

annotation/agent exposes @Stage, @Tool, and @ModelProvider. Their typed handlers return only Spice’s generic provider and bean-metadata contributions. Factory signatures remain ordinary Go and the generic compiler owns exact type identity and aliases. It transports canonical per-result facts; handlers validate their public interface contract and standard cleanup/error forms without parsing display strings. The authorized cmd/spice-agent-annotations process is protocol dispatch only; it is not an application registry. Typed stages use distinct stage.Stage[Input, Output] interfaces.

internal/compositionfixture is the executable architecture proof. Its committed internal/spicegen/compositionproof target constructs exact aliased model, tool, and generic stage interfaces through direct calls. The graph demonstrates fallback suppression, typed primary selection, alias-qualified injection, ordered slices, canonical named maps, reverse cleanup, rollback, and typed application-owned test overrides. Negative testdata graphs prove deterministic ambiguity and fail-closed concrete-to-interface injection.

Dependency direction

message tool interaction
\ | /
model
|
stage
|
event <- agent

Public contracts do not import repository internals. Protocol and host packages may depend on public contracts; the kernel does not depend on transport, UI, or provider implementations.

The public daemon package is the transport-independent host seam. A generated DefinitionSet wraps exact immutable agent.Definition values and owns the server-side model/turn policy. Stable cryptographic client identities use compare-and-swap ownership epochs derived from the daemon root context. A bounded per-client idempotency ledger commits canonical success, business failure, or secret-safe uncertain outcomes. PendingHub is both the kernel interaction broker and the stable-client-partitioned discovery source. Explicit run bindings route accepted prompts to one client; each subscription atomically captures that client’s sorted complete snapshot and gap-free revisioned tail. Reconnect fencing joins old observer delivery before returning. Global and per-client run, prompt, observer, and queue budgets bound retained state, and shutdown joins delivery while releasing every binding. These primitives contain no gRPC, Protobuf translation, listener, endpoint, or OS IPC behavior.

Every client epoch also owns a bounded commit and stream gate. Mutating work crosses an exclusive FIFO commit boundary; a reconnect intent takes priority, waits for the active commit, cancels old stream contexts, and cannot publish the next epoch until every old sender has joined and released its stream lease. Queued mutation, reconnect, and stream-acquisition claimants are hard-bounded and included in shutdown drain accounting. Known stale owners receive exact expected/observed epochs, while an unknown identity exposes no invented facts.

The public client package is the matching transport-independent consumer seam. It contains immutable negotiated connection, run, event, interaction, snapshot, health, and typed recovery values plus concurrent Connector, Session, and stream interfaces. It deliberately imports only the standard library. Successful replay/tail controls are explicit frames, arbitrary interaction JSON is bounded and secret-redacted by default, and every typed failure preserves the wire status’s common safe facts. Authentication, discovery, gRPC translation, and OS endpoint ownership belong to an adapter, not this contract or the TUI.

common/v1 and engine/v1 are the only initial Protobuf process boundary. They encode protocol negotiation, typed status, server-owned definitions, stable-owner reconnect, health, atomic run/event replay, complete-first pending interaction streams, cancellation, suspend/resume, and snapshot transfer. Authentication remains transport metadata. Handwritten validators fail closed before state mutation. The packages deliberately contain no listener, daemon, client lifecycle, registry, or translation into kernel internals. The quality gate rejects gRPC, Protobuf, and generated protocol imports from kernel packages.

Snapshot transfer is available only on protocol minor 1 with the negotiated snapshot-authority-v1 capability. Every envelope carries a 32-byte authority scope, positive key generation, and HMAC-SHA256 over canonical semantic fields. Construction requires a trusted signer and import requires a keyed verifier; the unkeyed validator checks structure and payload integrity only. Authority keys are not IPC authentication tokens and remain outside Protobuf, snapshots, events, logs, and errors.

daemon.RunAuthority implements that signer/verifier seam with a distinct current-user persistent key and scope, signed lifecycle records, and stable per-run OS locks. It retains the validated authority-directory object for its whole lifetime: Unix child operations are descriptor-relative and Windows child operations are handle-relative. Store shutdown rejects new leases and drains existing run/import leases before closing the bound directory. Import is deliberately transactional—verify, persist IMPORTING, commit the prepared kernel run, then persist ACTIVE—and any ambiguous persistence attempt makes that transaction terminally uncertain until it releases its lock.

Local resume has the inverse publication order needed to prevent snapshot replay without releasing execution early: Run.PrepareLocalResume reserves the exact suspended boundary, authority Resume durably invalidates its signed snapshot, and only then does the host commit the kernel reservation. Aborting the reservation before the authority transition restores the byte-identical suspended snapshot. Cancellation and engine shutdown latch behind the pending decision and cannot start post-boundary work.

Every interaction broker call carries an immutable validated run scope. Prompt content and response values stay out of authoritative run events; process clients discover them through an atomic complete pending snapshot followed by revisioned opened/closed deltas. The event log exposes count-and-byte-bounded pages whose bounds and optional final-head tail registration are captured under one lock, including empty initial [1,0] and imported [N+1,N] tails.

Every tool definition explicitly classifies external-state effect and replay safety and carries a canonically ordered capability set. Model-visible tool problems are normal correlated results and may continue the model loop; infrastructure failures are bounded correlated tool.ExecutionError values. An uncertain mutating outcome is distinct from a definitive failure and can never authorize automatic replay. These contracts are kernel-owned and apply equally to compiled and future runtime-plugin tools.

The engine acquires a tool plan only after public input validation and before ID allocation, event-log mutation, or execution. It releases the lease exactly once on every rollback and terminal path. Release precedes run-terminal persistence, making cleanup failure part of the single authoritative terminal state. Cancellation classification comes from the run/request context, not an arbitrary error cause returned by an extension.

Generated engines provide one semantic SnapshotCompatibilityIdentity and compiled identities for every executable provider, stage, observer, broker, static tool, and dispatcher decorator bean. Convenience constructors leave the compatibility identity empty: runs, local suspend/resume, and snapshot inspection work, but cross-engine ResumeSnapshot fails closed. A plan source is trusted to keep leased behavior immutable and never reuse a PlanID; the kernel can freeze definitions and guard names, but cannot clone Go behavior.

See docs/adr/0001-spice-native-composition.md for the rejected parallel graph.