Skip to content
Spice Framework on GitHub

RFC 0001: Deterministic Kernel Boundary

agentMaturity: experimentalSource: spice-agent@4726fb8Exact reviewed source
  • Status: accepted for preview
  • Applies to: github.com/spice-framework/spice-agent
  • Stability: pre-1.0

Problem

Agent frameworks tend to accumulate provider clients, tool registries, UI state, persistence, permission policy, and worker scheduling in one mutable runtime. That makes deterministic testing and safe extension difficult and would bypass Spice’s generated dependency graph.

Decision

The kernel owns only provider-neutral immutable values and one deterministic single-agent state machine. Engine.Start receives a validated definition and input. Providers, the canonical tool dispatcher, identifiers, clock, interaction broker, required observers, and best-effort observers are constructor-injected typed beans. The engine never receives or exposes a service registry.

The kernel owns:

  • immutable run, turn, message, model-operation, tool-call, and interaction IDs;
  • bounded messages and model/tool contracts;
  • turn/tool-loop state transitions;
  • event sequencing and authoritative replay;
  • cancellation and panic containment;
  • exactly-once lifecycle terminal finalization;
  • provider-neutral safe snapshots and import validation;
  • immutable static and leased dynamic execution-plan identity per run.

Provider implementations, tool implementations, daemon transport, UI, persistence, approval policy, sandboxing, Git/MCP/indexing/telemetry, and multi-agent scheduling remain outside the kernel.

Trust and failure semantics

Providers and compiled tools are trusted concurrent in-process beans. Context cancellation is cooperative; the kernel cannot force a function that ignores context to return. Panics are contained at provider, stream, dispatcher, and engine boundaries and normalized into terminal failure.

Each run acquires a ToolPlanLease after input validation and before ID allocation or event mutation. The host snapshots definitions; the trusted source guarantees stable executable behavior under a non-reusable PlanID. All model requests and calls for that run use the leased dispatcher. Generated compiled compatibility is checked before exact-plan resume and import mutation. Release callbacks are non-blocking reference decrements; source draining is asynchronous. The engine nevertheless bounds release, and timeout or failure forces the single authoritative RunFailed event, snapshot status, and Wait result.

All executable tools traverse one injected ToolDispatcher. The dispatcher publishes an immutable capability/definition snapshot and validates active call and progress correlation. Definitions explicitly classify external-state effect and replay safety; capabilities form an unordered set returned in canonical lexical order. Read-only definitions reject mutation-capable filesystem, process, network, and environment capabilities. No tool may retain a reporter after execution.

tool.Tool.Execute returns (tool.Result, error). A valid Result whose problem is set is model-visible terminal tool data and may continue the model loop. An infrastructure failure returns a zero result and exactly one direct, bounded, call-correlated *tool.ExecutionError; wrappers and joined siblings are rejected. The error distinguishes a definitive failure from an uncertain mutation outcome and supplies validated retry advice. The dispatcher rejects untyped, uncorrelated, contradictory, or result-plus-error outcomes and preserves errors.Is cancellation/deadline semantics. A successfully returned, valid correlated result remains authoritative when cancellation races after tool commit. Conversely, an extension returning a cancellation sentinel while the run context remains active is an ordinary run failure; lifecycle cancellation is derived only from authoritative context state.

Tool.Execute itself must return exactly one direct ExecutionError. If a progress reporter also failed, the dispatcher returns a bounded structured stage.DispatchFailure: ordinary unwrapping reaches only the authoritative execution outcome, while ReporterFailure exposes required-observer durability for explicit two-step inspection without contaminating errors.Is cancellation.

Local event commit precedes required-observer acknowledgement. Once committed, a sequence is never reused even if an observer reports failure. Bounded terminal finalization uses a context independent of caller cancellation and surfaces a typed durability error when completion cannot be established.

Dependency rules

Kernel packages may depend on other provider-neutral core packages and the Go standard library. They may not import OpenAI, coding tools, gRPC, Protobuf, Bubble Tea, OS IPC, SQLite, Spice compiler/CLI entrypoints, or distribution packages. Architecture tests enforce the direction.

Rejected alternatives

  • A runtime bean factory or service locator duplicates Spice and hides exact dependencies.
  • A compiled RuntimeGraph permits wiring to differ from generated Go.
  • Provider-specific message fields force all consumers to inherit provider churn.
  • Kernel parent/child agent concepts prematurely constrain orchestration.
  • Reflection dispatch weakens navigation, compile-time checks, and debugging.

Consequences

Extension authors sometimes write small adapters or decorators instead of registering arbitrary runtime objects. In exchange, embedded tests construct the entire engine with ordinary Go, generated applications are inspectable, and provider/tool/transport implementations evolve independently.

Acceptance evidence

The boundary is proven only when a real provider, compiled tool, runtime tool, required persistence observer, alternate client, and two-worker extension pass without adding implementation concepts to the kernel. Dependency scans, race tests, cancellation/panic tests, snapshot round trips, and generated DI source are retained in the phase ledger.