New @deepseek-ai/dsh-invariants plugin (pure listeners, off in prod) asserts the event taxonomy at runtime — seq monotonicity, turn/step nesting, a tool/result needs a prior tool/call (NOT the converse), legal agent/status transitions — and deep-freezes logged event data so mutating history throws. Seeded sessions are checked + frozen on session/created. The real RFC 008 fix is always-on: deriveMessages now structured-clones the content it emits, so the loop's sanctioned request/adapter mutation can no longer reach back and rewrite the append-only log. The pervasive DeepReadonly<T> type flip is rejected (compile-only, high-noise, castable) — recorded in ADR 0012, which folds in RFC 008. Wired into both demos.
2.4 KiB
2.4 KiB
RFC 005: Runtime validation at the model boundary, error taxonomy, dev-mode invariants
Status: partially implemented — part 1 (arg validation) → ADR 0011; part 3 (dev invariants) → ADR 0012; part 2 (error taxonomy) in progress
Problem
Three gaps where compile-time guarantees stop:
- Tool args are model-generated JSON —
defineTool'sInferArgs<S>claim is only as true as the model's output. Today a malformed call reachesexecuteuntyped-in-practice. - Tool errors flatten to a text block; name/code/stack are lost, so future sandbox/retry plugins can't distinguish ENOENT from EACCES, and the model gets less actionable feedback than it could.
- Loop ordering invariants (seq monotonicity, step/turn event nesting, turn-number continuity) are asserted only where tests look.
Proposal
- Schema validation in defineTool: before
execute, validate parsed args against the SchemaSpec (the converter already encodes the structure — a small interpreter walks it: presence of required keys, primitive type checks, enum membership, recursion into objects/arrays). On mismatch, return anisErrorToolExecutionResult describing the violation — the model can self-correct. Raw-registered tools (MCP) keep validating their own input. - Structured error taxonomy: per-package error classes extending a common
HarnessError(name,code,causechaining).ToolExecutionResultgains optionalerror: { name, code }alongside the model-facing text. The loop'serrorDataconsumes it; sessionerrorevents carry the code. This also properly fixes the non-Error-throw message degradation found in review. - Dev-mode invariants: a
dsh-invariantsdebug plugin (everything is a plugin — it's just listeners) asserting, when enabled: session seq strictly increases;step/startprecedes its chunks;turn/start/turn/endpair and nest; tool/call has a matching tool/result; status transitions are legal. Enabled in tests and the demo; off in production. Doubles as executable documentation of the event contract.
Plan
2 first (taxonomy is a dependency of 1's error shape), then 1, then 3. Property tests (RFC 001) then close the loop: generated args ↔ validator ↔ InferArgs agreement.
Risks
Validator/InferArgs drift — covered by the RFC 001 composition property. Validation cost per call is negligible next to a model call.