# RFC 0001: Deterministic Kernel Boundary

- **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.
