Middleware¶
A middleware is an object implementing any subset of the hook points. The kernel composes them into an onion; order is explicit and inspectable. Every feature outside the kernel is a middleware, a backend, or an adapter — if a feature needs to edit the kernel, it was modeled wrong.
Writing one¶
Subclass Middleware (no-op defaults) or just implement the hooks you need:
from spine_core import Middleware, StepContext
class Logging(Middleware):
async def before_model(self, ctx: StepContext) -> None:
print(f"step {ctx.state.step}: {len(ctx.messages)} messages")
async def after_model(self, ctx: StepContext) -> None:
print("usage:", ctx.response.usage)
Order matters¶
before_* hooks run outermost-first (list order); after_* hooks run
innermost-first (reverse). A wrapping middleware brackets the ones it encloses.
middleware=[Retry(), Guardrails(), Compaction(), OTel()]
# outer ───────────────────────────► inner (before_model)
# inner ◄─────────────────────────── outer (after_model)
Control flow¶
Middleware can steer the run, not just observe it:
- Stop the run — raise
StopRun(reason, message); the kernel turns it into a clean stoppedResult(used byLoopGuard, guardrails, budgets). - Force another turn — set
ctx.force_continue = Trueto loop again even without a tool call (used byStructuredOutputfor repair). - Short-circuit the provider — preset
ctx.responseinbefore_model(used byCacheandReplayer). - Skip / preset a tool — set
ctx.skip+ctx.resultinbefore_tool(used byIdempotency,Replayer,Sandbox). - Handle errors —
on_errorreturnsretry/fallback/skip/fail(used byRetry,ModelFallback,CircuitBreaker).
Configure by name¶
Registered middlewares resolve from spine.toml:
[spine.middleware]
chain = ["Retry", "CostTracking", "LoopGuard"]
[spine.plugins.CostTracking]
input_per_mtok = 0.15
output_per_mtok = 0.60
See the full middleware catalog.