5.2 KiB
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.
SubWorkflowversion resolution is T3.6's task; the interpreter consumes an already-resolved hash.
Steps
- Replace T1.3's hardcoded step list with a walk over
WorkflowDef.stepsdriven bytransitions. Keep the run lifecycle from T1.3 unchanged. Model: renderPromptTemplate, capture the context partition (T1.5), put prompt and output blobs (T1.6), recordUsage.Tool: resolveToolId, renderArgTemplate, call through the intent wrapper (T2.1), record the tool call as its own step with identity, arguments and capabilities.Parallel: fan out inside theRunScope(T1.1) so cancellation still propagates; applyJoinPolicyat the join.Conditional: evaluatePredicateagainst recorded state only — no ambient inputs, or the fold stops being reproducible.SubWorkflow: execute against the resolved version, within the kernel's recursion bound, recording the child run's linkage.- 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:
- Express P1's hardcoded workflow as YAML. Run it through the interpreter.
- 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.
- 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,JoinPolicyapplied; 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.
- Serialization guard: instrument step start/end times; assert no two
non-
Parallelsteps of one run overlap. - Bypass audit: assert every tool invocation in the run has a matching
Pending/Dispatched/Committedintent triple. A count mismatch means a direct call path exists. assert_refold_identicalon 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.
Paralleltested 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