(tasks) add tasks for harness
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# T2.2 — Effect-class recovery
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Phase | P2 — Durability hard parts |
|
||||
| Size | M — 1 to 3 days |
|
||||
| Status | Not started |
|
||||
| Flags | — |
|
||||
| Spec | inlined below |
|
||||
| Blocks | T6.5, T8.3 |
|
||||
|
||||
## Goal
|
||||
|
||||
Resolve a `Dispatched`-but-unresolved intent by the tool's declared effect class.
|
||||
`Indeterminate` becomes a real terminal state with an operator path.
|
||||
|
||||
## Facts (inlined — no spec read needed)
|
||||
|
||||
```rust
|
||||
pub struct ToolRegistration {
|
||||
pub id: ToolId,
|
||||
/// No Default. The author is the only party who knows this.
|
||||
pub effects: EffectClass,
|
||||
pub caps: CapabilitySet,
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
/// Each class carries what its recovery path actually needs. A bare
|
||||
/// discriminant would let a tool claim `Idempotent` while withholding the one
|
||||
/// thing that makes the claim actionable.
|
||||
pub enum EffectClass {
|
||||
Idempotent { key: KeyDerivation },
|
||||
Queryable { lookup: RequestIdLookup },
|
||||
Unsafe,
|
||||
}
|
||||
```
|
||||
|
||||
Resolution for a `Dispatched` intent:
|
||||
|
||||
| Class | Recovery |
|
||||
|---|---|
|
||||
| `Idempotent` | retry with the same idempotency key; the provider deduplicates |
|
||||
| `Queryable` | ask the provider whether the request id landed, then complete or retry |
|
||||
| `Unsafe` | **never auto-retry.** Attempt becomes `Indeterminate`, operator notified |
|
||||
|
||||
- The third row is the honest one. Some effects cannot be made safe by any
|
||||
protocol. The intent log's value is converting an invisible unknown into a
|
||||
recorded one: `Indeterminate` is a fact a grader and an operator can both use;
|
||||
a silently retried payment is not.
|
||||
- A tool that cannot state whether it is safe to retry **does not register**.
|
||||
- Declaration is not enforcement — that is T8.3's keyed capability, and the
|
||||
sandbox (T6.5) is the third layer. This task builds layer one plus recovery.
|
||||
- Until built-in tools are audited, default `Unsafe` and never auto-retry.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Define `EffectClass` with payloads as above — no bare discriminants. A
|
||||
registration declaring `Idempotent` without a `KeyDerivation` must not compile.
|
||||
2. Extend `ToolRegistration` and reject registration of a tool with no declared
|
||||
effect class. No `Default` impl.
|
||||
3. Implement the recovery pass, driven by T2.1's restart scan: for each
|
||||
`Dispatched` intent, dispatch on the registered class.
|
||||
4. `Idempotent`: re-derive the key from the recorded arguments, re-issue, record
|
||||
the outcome. Same key, so the provider deduplicates.
|
||||
5. `Queryable`: call the registered `RequestIdLookup` with the recorded request
|
||||
id; complete from the provider's answer or retry if it never landed.
|
||||
6. `Unsafe`: transition the attempt to `Indeterminate`, emit the operator
|
||||
notification, and **stop**. No retry path exists for this arm — do not add one
|
||||
behind a flag.
|
||||
7. Build three test tools, one per class, each with an injectable crash point
|
||||
mid-call.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Three tools, one per class; crash mid-call for each.
|
||||
- `Idempotent` retries and produces no duplicate effect.
|
||||
- `Queryable` reconciles against the provider and completes or retries correctly.
|
||||
- `Unsafe` becomes `Indeterminate` and raises, rather than retrying.
|
||||
|
||||
## Verify
|
||||
|
||||
**Harness:** three real test tools, one per class, each writing to an
|
||||
**external** side-effect ledger (a file or a counter service) so effect count is
|
||||
observable independently of the log. Plus T2.1's child-process kill.
|
||||
|
||||
**Integration test** — `tests/it_effect_recovery.rs`:
|
||||
1. **`Idempotent`:** tool appends to the ledger keyed by its idempotency key.
|
||||
Kill mid-call, restart, let recovery retry. Assert the ledger holds
|
||||
**exactly one** entry and the attempt completes.
|
||||
2. **`Queryable`:** tool records a request id; the fake provider is configurable
|
||||
to answer "landed" or "never arrived".
|
||||
- Provider says landed → assert recovery **completes** without re-issuing;
|
||||
ledger count stays 1.
|
||||
- Provider says never arrived → assert recovery **retries**; ledger count
|
||||
becomes 1 (from the retry, not two).
|
||||
3. **`Unsafe`:** kill mid-call. Assert the attempt becomes `Indeterminate`, the
|
||||
operator notification fires, and the ledger count is **unchanged** — no retry
|
||||
was issued.
|
||||
4. Registration: a tool with no declared effect class is rejected; a
|
||||
`Idempotent` registration without a `KeyDerivation` fails to compile
|
||||
(`trybuild`).
|
||||
5. Key stability: run recovery twice; assert the derived key is identical both
|
||||
times.
|
||||
|
||||
**Command:** `cargo test -p durability --features test-hooks effect_recovery -- --test-threads=1`
|
||||
|
||||
**False pass:**
|
||||
- Counting effects only in the framework's own log. The log is what you are
|
||||
testing; the external ledger is the witness.
|
||||
- The `Idempotent` case passing because the provider stub deduplicates on
|
||||
*content* rather than on the supplied key — it then passes even when the key
|
||||
derivation is broken. Make the stub key-only.
|
||||
- Testing `Unsafe` by asserting an error is returned. `Indeterminate` is a state,
|
||||
not an error, and the distinction is the deliverable.
|
||||
|
||||
## Traps
|
||||
|
||||
- A "retry anyway with a warning" path for `Unsafe`. The class exists to say the
|
||||
answer is no.
|
||||
- Deriving the idempotency key at recovery time from something that changed — it
|
||||
must derive from the recorded arguments, deterministically.
|
||||
|
||||
---
|
||||
|
||||
Background (not required to do this task):
|
||||
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §8.4, §13.1, §13.2 ·
|
||||
[rust-agentic-task.md](../../../rust-agentic-task.md)
|
||||
Reference in New Issue
Block a user