113 lines
4.7 KiB
Markdown
113 lines
4.7 KiB
Markdown
# 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)
|