Files

113 lines
4.7 KiB
Markdown
Raw Permalink Normal View History

2026-08-18 20:04:13 -07:00
# T10.2 — `Reconciler` port
| Field | Value |
|---|---|
| Phase | P10 — Orchestration |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | T10.6, T10.8 |
## Goal
New port answering "how far toward the goal, what next" from a run's terminal
resting state — distinct from `Verifier` (fail-closed pass/fail) and `Judge`
(relative-only comparison).
## Facts (inlined — no spec read needed)
- `Verifier::verify` is fail-closed pass/fail (rust-agentic-sys.md §10).
`Judge::compare` is deliberately relative-only, "cannot return an absolute
score" (rust-agentic-sys.md §11.1). "How far toward goal, what next" is a
third question — forcing it through either port blurs a firewall built on
purpose. `Reconciler` is new, sized like `Verifier` (one method, S/M).
- Fires **only** off `Verified{pass|fail}`, `Graded`, `Ungraded` — states
`T1.3` already designed to rest in, never resolved inline.
- Reconciler failure degrades the record, never the run — same principle as
§1: "the framework cannot break the agent it runs." A `Reconciler` error or
timeout leaves the run at its terminal state; the goal simply does not
advance until retried.
- The decision does **not** spawn anything itself. Spawning happens only
through the admission path (T10.3) via the inbox relay (T10.7) — never a
direct call from the `Reconciler` or its caller.
- One new `#[non_exhaustive]` `WorkEvent` variant for provenance:
`SpawnedFromReconciliation{goal_id, parent_run, decision_ref}`. Added, not
replacing any existing variant.
## Steps
1. Define `ReconcileCtx``GoalId`, `GoalView` (T10.1), the just-terminal
run's `EpisodeView`, `WorkflowDef`.
2. Define `ReconcileDecision``SpawnNext{workflow_ref, input, dispatch_key}`,
`Retry{step}`, `Stop{reason}`.
3. Define the trait:
```rust
#[async_trait]
pub trait Reconciler: Send + Sync {
fn id(&self) -> ReconcilerId;
async fn reconcile(&self, cx: &ReconcileCtx) -> Result<ReconcileDecision>;
}
```
4. Wire the trigger: on a run reaching `Verified`/`Graded`/`Ungraded` with a
`GoalId` set, call the `Reconciler` bound to that goal.
5. On `Reconciler` error or timeout: log, leave the run at its terminal state,
do not auto-retry — matches `Verifier`'s explicit-only-retry discipline.
6. Add `WorkEvent::SpawnedFromReconciliation`, written on the **next**
spawned run's first log record, referencing `parent_run` and the decision.
7. Test with a stub `Reconciler` against all three trigger states.
## Acceptance
- A run reaching `Verified{pass}` with a `GoalId` triggers exactly one
reconcile call.
- The decision is durably recorded and traceable from the next spawned run
back to the parent run and goal.
- A `Reconciler` error leaves the run at its terminal state and does not
crash the executor.
## Verify
**Harness:** stub `Reconciler` returning each decision variant in turn; one
goal with a 2-run chain.
**Integration test** — `tests/it_reconciler_trigger.rs`:
1. Run A (goal G) reaches `Verified{pass}`; assert `reconcile()` called
exactly once with the correct `ReconcileCtx`.
2. Stub returns `SpawnNext`; assert the decision is persisted, referencing
run A.
3. Assert the next spawned run's first log record carries
`SpawnedFromReconciliation{goal_id: G, parent_run: A, decision_ref}`.
4. Repeat for `Graded` and `Ungraded` trigger states.
5. Stub `Reconciler` panics or times out; assert run A stays at its terminal
state, no crash, no infinite retry.
6. Run with no `GoalId` reaches `Verified{pass}`; assert `reconcile()` is
never called.
**Command:** `cargo test -p kernel reconciler_trigger`
**False pass:**
- Asserting reconcile "ran" via a boolean flag instead of asserting the
`WorkEvent` provenance record exists — passes even if a crash lost the
decision before persistence.
- Testing only `Verified{pass}`, skipping `Graded`/`Ungraded` — those are the
states most likely miswired, being newer additions to the FSM.
## Traps
- `Reconciler` spawning the next run directly. Violates "never bypass the
admission path" — see T10.3/T10.7.
- Overloading `Judge::compare` to also answer "what next." Collapses a
boundary `Judge`'s signature deliberately enforces (rust-agentic-sys.md
§11.1, "deliberately cannot return an absolute score").
- Auto-retrying a failed `Reconciler` call with no cap — a broken impl then
spins forever against every terminal run in the tenant.
---
Background (not required to do this task):
[rust-agentic-sys.md](../rust-agentic-sys.md) §1, §10, §11.1, §17 ·
[T1.3-run-executor.md](T1.3-run-executor.md) ·
[T4.1-verifier-port.md](T4.1-verifier-port.md) ·
[T10.1-goalid-and-goal-scoped-query.md](T10.1-goalid-and-goal-scoped-query.md)