(plan) update the plan to captures more
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user