Files
poimen/tasks/T3.5-interpreter-over-the-ir.md
T

124 lines
5.2 KiB
Markdown

# T3.5 — Interpreter over the IR
| Field | Value |
|---|---|
| Phase | P3 — Workflow as data |
| Size | L — over 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
## Goal
Execute a validated `WorkflowDef` directly. Replaces T1.3's hardcoded workflow.
## Facts (inlined — no spec read needed)
Five step kinds to support:
```rust
pub enum StepKind {
Model { prompt: PromptTemplate, effort: ReasoningEffort },
Tool { tool: ToolId, args: ArgTemplate },
Parallel { branches: Vec<StepId>, join: JoinPolicy },
Conditional { on: Predicate, then: StepId, otherwise: Option<StepId> },
SubWorkflow { workflow: WorkflowId, version: VersionSelector },
}
```
- Concurrency shape is fixed and the interpreter must not widen it:
| Scope | Parallel |
|---|---|
| across runs | unbounded |
| steps within a run | **serial by default** — step N+1 reads N's output |
| `Parallel` step branches | fan-out/join, declared explicitly in the IR |
| attempts within a step | **strictly serial** — a retry needs the prior failure |
| verifiers for one attempt | fan-out/join |
- The serial spine is `(TenantId, RunId)`. Parallelism lives between runs and
inside declared fan-out. Nothing else may interleave.
- **Tool calls appear in the episode as first-class steps.** The hostcall
boundary already knows the identity, arguments and capabilities; recording them
as opaque invocations discards information the runtime is holding anyway.
- Every external effect goes through the write-ahead intent wrapper (T2.1) — the
interpreter is where that is easy to bypass.
- `SubWorkflow` version resolution is T3.6's task; the interpreter consumes an
already-resolved hash.
## Steps
1. Replace T1.3's hardcoded step list with a walk over `WorkflowDef.steps` driven
by `transitions`. Keep the run lifecycle from T1.3 unchanged.
2. `Model`: render `PromptTemplate`, capture the context partition (T1.5), put
prompt and output blobs (T1.6), record `Usage`.
3. `Tool`: resolve `ToolId`, render `ArgTemplate`, call through the intent
wrapper (T2.1), record the tool call as its own step with identity, arguments
and capabilities.
4. `Parallel`: fan out inside the `RunScope` (T1.1) so cancellation still
propagates; apply `JoinPolicy` at the join.
5. `Conditional`: evaluate `Predicate` against recorded state only — no ambient
inputs, or the fold stops being reproducible.
6. `SubWorkflow`: execute against the resolved version, within the kernel's
recursion bound, recording the child run's linkage.
7. Port P1's hardcoded workflow to YAML and diff the resulting episode structure
against the P1 fixture.
## Acceptance
- P1's hardcoded workflow, re-expressed as YAML, produces an **identical episode
structure**.
## Verify
**Harness:** P1's episode fixture as the oracle, plus the stub model and the
external side-effect ledger from T2.2.
**Integration test**`tests/it_interpreter_parity.rs`:
1. Express P1's hardcoded workflow as YAML. Run it through the interpreter.
2. Assert the resulting **episode structure** matches the P1 fixture: same steps,
same attempt counts, same event sequence. Compare the serialized episode with
volatile fields (ids, timestamps) normalized — and list the normalized fields
explicitly, so the normalizer cannot quietly grow.
3. One test per `StepKind`:
- `Model` — prompt rendered, partition and blobs captured.
- `Tool` — appears as a **first-class step** with identity, arguments and
capabilities recorded; the effect went through the intent wrapper (assert 3
intent records exist).
- `Parallel` — branches run concurrently, `JoinPolicy` applied; cancel the run
mid-fan-out and assert every branch task terminated.
- `Conditional` — both arms exercised; run the same workflow twice and assert
identical predicate outcomes.
- `SubWorkflow` — child linkage recorded; recursion past the bound rejected.
4. **Serialization guard:** instrument step start/end times; assert no two
non-`Parallel` steps of one run overlap.
5. **Bypass audit:** assert every tool invocation in the run has a matching
`Pending`/`Dispatched`/`Committed` intent triple. A count mismatch means a
direct call path exists.
6. `assert_refold_identical` on the interpreted run.
**Command:** `cargo test -p executor interpreter`
**False pass:**
- Step 2 with an over-broad normalizer that blanks anything differing. Then any
two episodes "match". Enumerate the normalized fields.
- Step 5 omitted: calling a tool directly works, produces a correct-looking
episode, and fails T2.6's crash matrix later in a way that looks unrelated.
- `Parallel` tested with branches that complete instantly, where concurrency is
indistinguishable from serial execution.
## Traps
- Running independent-looking steps concurrently. Serial-by-default is the
contract; fan-out is declared, never inferred.
- Calling a tool directly rather than through the intent wrapper. It works, and
T2.6's crash matrix then fails somewhere unrelated-looking.
- A predicate that reads the clock or the environment.
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §4.1, §5.3, §13.1 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)