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

5.2 KiB

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:

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 testtests/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 §4.1, §5.3, §13.1 · rust-agentic-task.md