Skip to content

sherpa: evidence-driven recursive agentic runtime MVP (#492) - #493

Open
jeremymanning wants to merge 19 commits into
mainfrom
sherpa
Open

sherpa: evidence-driven recursive agentic runtime MVP (#492)#493
jeremymanning wants to merge 19 commits into
mainfrom
sherpa

Conversation

@jeremymanning

Copy link
Copy Markdown
Member

Closes #492.

New experimental package src/sherpa/ beside the frozen supported path, implementing the MVP scope end to end:

  • Typed plan IR (ir.py): nine structural node variants (InvokeCapability, InvokePlan, Decompose, Branch, bounded While, Parallel, AskUser, Return, Fail), budgets, glob-subset authority delegation; fail-closed AST-whitelist expressions — no eval() anywhere.
  • Durable kernel (kernel.py, store.py): single append-only SQLite WAL event log as source of truth; projections (runs/nodes/leases/messages/usage/cache/FTS5/summaries/findings) rebuilt by replay; worker leases with TTL + orphan recovery; checkpoint-boundary message delivery; loud terminal states including budget_exhausted; resume-by-replay skips completed idempotent nodes.
  • Admission control: an atomic step is a checked claim — capability existence → typed I/O → authority subset → executable probe evidence hashed into the blob store; rejections reclassify to decomposition or escalate. Overclaim is measured from events.
  • Bounded independent review: session separation enforced; frozen concern ledger; deterministic checks against the problem contract; blocking requires criterion + reproducible evidence (unevidenced reviewer claims are downgraded and marked invalid); dispositions; caps; verdict is pass_with_risk or blocked_escalated, never "clean".
  • Context substrate: operational journal, exact-span chunking, summary DAG where every summary points at all children with spans/hashes, FTS5 retrieval, lock-free scoped snapshots.
  • Model boundary (channel.py): RecordedChannel (hermetic replay), LiveChannel (lazy Dartmouth/HF only), EchoChannel (loud refusal).
  • Three demonstrations + measurement harness: durable-semantics fixture (real SIGKILL crash/resume), repository repair across a preregistered defect grammar with held-out variants, oversized-corpus synthesis with claim-to-span citations; corrected branching m = b·f, overclaim rate, bootstrap CIs; preregistered go/no-go gates.

Deliverables included: ADR 0003, schemas+validators (Pydantic IR + event vocabulary), kernel/CLI/replay tooling, benchmark harness, fault-injection/property/replay/authority/budget/retrieval/end-to-end tests, measurement report with raw artifacts (benchmarks/artifacts/), go/no-go statement.

Verification: sherpa suite 121 passed hermetically; supported-path blocking gate unchanged (1053 passed, 13 skipped). All seven preregistered gates PASS → GO on this fixture distribution (raw numbers in the first comment).

Design reviews/simulations from #485/#486 informed six encoded findings (negative-result caching, non-cancelling siblings, planner purity, budget-exhaustion ≠ bad-plan evidence, checked atomicity, measured overclaim).

- ir.py: Pydantic v2 plan IR — ProblemSpec, Budgets, Authority glob-subset
  delegation, nine structural node variants, validate_plan/estimate_depth
- expr.py: ast-whitelisted expression evaluator; calls/comprehensions/
  dunder attributes rejected at compile time; no eval() anywhere
- events.py: closed vocabulary of event kinds for the append-only log
Append-only event log is the source of truth; runs/nodes/leases/messages/
usage/cache/FTS5-chunks/summaries/findings are projections maintained
transactionally alongside appends and independently rebuildable by replay.
CAS protects state transitions only; budget exhaustion is refused as
negative plan evidence; WAL allows concurrent readers.
- channel.py: RecordedChannel (hermetic replay of the external boundary),
  LiveChannel (lazy Dartmouth/HF adapters only), EchoChannel (loud refusal)
- capabilities.py: typed specs, authority enforcement, executable probes,
  real fs/pytest/patch/FTS built-ins; run_capability journals both sides
  of every call and hashes outputs into the blob store
- admission: capability existence, typed I/O, authority subset, then
  EXECUTABLE EVIDENCE via probe; failures reclassify to decompose or
  escalate; every check journals admission_checked for overclaim metrics
- planner: deterministic plan_signature cache keys; StubPlanner over a
  declarative library; LLMPlanner with one repair round validating against
  IR schema AND parent authority
- context: operational journal, exact-span chunking, cited summary DAG
  (direct child pointers + transitive source spans), FTS retrieval,
  lock-free scoped snapshots
- review: session separation enforced; frozen concern ledger hashed at
  start; deterministic checks (plan validity, authority/budget containment,
  real acceptance results) produce evidenced blocking findings; model
  claims without evidence are downgraded to residual risks and marked
  invalid; dispositions persist; caps on rounds/tokens/time; verdict is
  pass_with_risk or blocked_escalated, never 'clean'
- metrics: corrected m=b*f, overclaim rate from admission_checked events,
  deterministic bootstrap CIs, markdown report renderer
)

- Engine.run/resume/status/export_trace/deliver_message over the event log
- crash-safe: REAL SIGKILL mid-run then resume completes with exactly-once
  effects, projection equivalence vs an uninterrupted reference run
- epoch-qualified node keys make While iterations distinct and resumable;
  loop counters are event-sourced (node_progress)
- blocked is loud-but-continuable for attended runs; final states refuse
- admission gates every leaf; reclassified claims spawn child plans;
  final outputs pass real acceptance checks + independent review before
  completed may be reported; budget exhaustion stops loudly
- A durable-semantics fixture: REAL SIGKILL mid-run, scope-change message
  consumed at a checkpoint after resume, seeded contract defect caught by
  the independent review gate, plan v2 completes; exactly-once effects and
  projection equivalence vs an uninterrupted reference run
- B repository repair across the preregistered defect grammar incl.
  held-out variants; evidence-driven RepairPlanner authors concrete diffs;
  acceptance is a REAL pytest execution outside the runtime
- C oversized-corpus synthesis: FTS needle recall, claim-to-span citations,
  summary-routed addressability
- decomposition battery feeds corrected m=b*f and overclaim measurements;
  harness evaluates preregistered gates and prints go/no-go
- tests/sherpa/test_acceptance.py runs all three demonstrations through the
  public API in the blocking gate (reduced matrix; full matrix via
  python -m sherpa.benchmarks.harness)
- committed raw run artifacts + measurement report: all seven preregistered
  gates PASS -> GO on this fixture distribution
@jeremymanning

Copy link
Copy Markdown
Member Author

Run evidence — full preregistered matrix (python -m sherpa.benchmarks.harness)

All numbers below are projections from the committed raw artifacts in benchmarks/artifacts/ (per-run traces, scenario JSONs, suite.json, report.md). No assumed values.

Preregistered gates — 7/7 PASS → GO

gate threshold observed verdict
decomposition decisions with independent admission outcomes >= 50 66 PASS
claimed-atomic steps admitted/rejected independently >= 30 98 PASS
held-out repair/corpus tasks externally verified within budgets >= 80% 1.0 PASS
corrected branching m = b·f upper bound on fixture distribution < 1.0 0.0 PASS
seeded-needle retrieval recall >= 95% 1.0 PASS
crash/resume preserves projections; no repeated effects required met PASS
repair results verified by REAL pytest outside the runtime no false successes 16/16 completed runs verified, 0 false successes PASS

GO: all preregistered MVP gates met on this fixture distribution. Thresholds are MVP decisions, not product claims.

Demonstration A — durable semantics (real SIGKILL)

  • Child process killed by actual SIGKILL mid-run (SHERPA_KILL_AFTER_EVENTS=22, returncode == -9 asserted).
  • Scope-change message enqueued while the run was down is consumed at a checkpoint boundary after resume (scope_change_consumed_after_resume: true).
  • Seeded contract defect: v1 Return declares lines_expected == 3 while the frozen acceptance predicate demands 4 → independent review blocks with an evidenced finding; plan v2 aligning the contract completes.
  • Resumed ledger vs uninterrupted reference ledger: identical sequences (run-open → child-checkpoint → poll ×2 → fast), exactly_once_effects: true, projection_equivalent: true (replay-built projection matches live tables and terminal state).

Demonstration B — repository repair (real pytest as external verifier)

16 variants: 4 defect classes × {seen seeds 11, 23} ∪ {held-out seeds 401, 409}. Held-out variants share the preregistered grammar with unseen function names/values — success on them evidences generalization, not scripting.

  • 16/16 completed, each repaired repo re-verified by pytest executed outside the runtime afterwards: 0 false successes.
  • Example trace lineage (committed): problem → plan_recorded → admission_checked per leaf → tool_call_started/finished (apply_patch diff + run_tests stdout hashed into blob store) → acceptance check → review round → run_terminal: completed.
  • 98 atomic claims independently admitted/rejected across the suite; measured overclaim rate feeds the report.

Demonstration C — evidence-grounded corpus task

  • Corpus: 40 docs / ~90 KB ≫ demo context window; 8 unique seeded needles + distractor-heavy filler.
  • Needle recall via FTS5: 100% (≥95% gate); every emitted claim resolves to an immutable chunk sha that contains the cited fact; routing cost measured in FTS queries; summary-routed addressability probed at depth 1.

Test suites

$ .venv/bin/python -m pytest tests/sherpa -q
121 passed

$ .venv/bin/python -m pytest tests/ -m "unit or contract or e2e" --ignore=tests/sherpa
1053 passed, 13 skipped   # supported path unchanged

Notable engineering findings during verification (each fixed + regression-covered)

  1. A child-plan Return used to terminate the whole run — root-only terminal semantics now.
  2. Loop iterations shared node keys, letting the completed-node skip swallow passes — epoch-qualified keys, event-sourced counters.
  3. blocked is loud-but-continuable (attended runs); only final states refuse resume.
  4. Same-size patches (<>, digit swap) could leave a stale __pycache__ that mtime+size validation missed — apply_patch now invalidates sibling bytecode caches. This was an intermittent, genuinely subtle flake.
  5. Unified-diff parser initially ate blank context lines; task generation originally used salted hash() — both fixed for determinism.

Honest scope notes

jeremymanning and others added 10 commits August 23, 2026 15:21
- __init__: Engine/ProblemSpec/RunResult/IR types on the public surface
  (quickstart in docs/sherpa.md verified against a real run)
- docs/sherpa.md: install, Python API, CLI, benchmarks, links to ADR 0003
Bare 'pytest' on this machine resolves to anaconda's Python 3.9, where
Pydantic cannot evaluate the IR's PEP 604 annotations and collection dies
with eight cryptic TypeErrors. The sherpa conftest now exits immediately
with the working invocation; docs lead with '.venv/bin/python -m pytest'.
Naive sum() over identical floats drifts platform-dependently on CPython
3.11 (twenty 0.4f -> 0.4000000000000001), so bootstrap_ci could exclude
the very constant it summarizes. All metric means now use math.fsum;
regression assertion added with a series that drifts on every interpreter.
demos.py drives planner-shaped decomposition, the fail-closed expression
evaluator, durable execution with admission outcomes, loud authority
escalation, and event-derived metrics - printing real transcript output;
committed transcript embedded in docs/sherpa.md.
Section 6 now forks a victim armed with SHERPA_KILL_AFTER_EVENTS, shows
the -9 exit code and partial on-disk effects at death, resumes via a
fresh engine, and proves exactly-once effects from the committed
transcript instead of pointing at the test suite.
#493)

Audit of the MVP found authority enforcement was non-functional. Three
divergent matchers answered the same question differently, and the one used as
the gate reduced to `candidate.startswith("")` for any grant ending in `*`, so
every wildcard grant allowed everything. Nothing in the package normalized a
filesystem path, so `..`, absolute paths, and symlinks escaped every grant.
Demonstrated end-to-end with grant `src/**`: reading an arbitrary absolute path
outside the workspace, following a symlink out, and overwriting a file outside
the workspace all succeeded.

- new `sherpa.authority`: ONE implementation, used by delegation, admission and
  execution alike. Separates delegation (pattern subset) from access (resolved
  resource containment). Paths are resolved before comparison, so traversal and
  symlinks are visible to the check rather than hidden by it.
- `**` is now workspace-relative; `/**` is the only way to ask for the whole
  filesystem. The documented quickstart grant can no longer reach /etc.
- `CapabilitySpec.requires` names authority *dimensions*; fs capabilities no
  longer declare `authority_required=fs_read=("**",)`, which had made every
  scoped grant refuse the capability outright. Scoped grants now work.
- repo.run_tests authority-checks its cwd (pytest executes conftest.py from it)
  and rejects code-loading pytest args.
- repo.apply_patch authorizes every target before writing any of them; it used
  to write the file and then raise.
- probes are authority-bearing: the read probe no longer writes a canary under
  a read-only grant, and write probes use unique names that cannot clobber.
- capability inputs are redacted in the event log instead of journaled verbatim.

Also in this commit, from parallel TDD workstreams:
- expr: BoolOp short-circuits; and/or return the operand; attribute access is
  mapping-only, closing the gi_frame -> f_builtins traversal; attr-chain depth
  counted correctly. (10 -> 82 tests)
- store: full-sha256 digest pinned by test (truncation previously survived the
  whole suite); findings no longer leak across runs; replay rebuilds findings
  from events; FTS5 queries tokenized and quoted (hyphenated terms crashed);
  lease owner may re-acquire; causal_seq persisted. (91 tests)

Tests: 121 -> 373 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
…ts (#493)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
…ree (#493)

Kernel half of the audit. Each item had a demonstrated failure before the fix.

- exactly-once: `_begin_attempt` returned True when `acquire_lease` FAILED, so a
  second worker executed a node another live session already held. The lease was
  correct; the kernel ignored its answer. Refusals are now honoured and recorded
  as a `lease_denied` event.
- recursion depth was a local initialised to 0 and never incremented, and
  `_author_child`'s depth argument was dead, so every node recorded depth 0 and
  `budgets.max_depth` bounded only structural branch/while nesting -- recursive
  decomposition was unbounded. Depth and the owning decompose node now travel on
  the execution stack and are persisted.
- the organization tree is now real: `parent_key` and `owner_session` are written
  for every node. Both were NULL for every node even after a real decomposition,
  and `cas_node_state` blanked `owner_session` on each later transition.
  `replay_projection` reconstructs both, so projection == replay still holds.
- loud terminals: planner failures (`NoPlanTemplate`, `PlanAuthoringError`) and
  plan-gate refusals escaped `Engine.run`, leaving the run stranded in `running`
  with no `run_terminal` event and its node pending. They are terminal states now.
- `decompose_outcome` was emitted only when the parent was not `skipped` -- which
  is exactly what the reclassify path sets -- so every admission CORRECTION was
  structurally invisible to the corrected branching factor. Always emitted, and
  tagged `reclassified`.
- model spend is recorded, so `max_tokens`/`max_cost_usd` can bind at all.
  `LiveChannel` reported 0 tokens always; estimates are now flagged
  `tokens_estimated` rather than passed off as measured, and Dartmouth failures
  are reported instead of swallowed.
- admission's probe no longer shares the executor's model session, which had it
  consuming two recordings per summarize and handing the step the second.
- `max_cost_usd` default 0.0 meant UNLIMITED spend (the check was skipped unless
  > 0); it now means zero. Budgets reject negative ceilings.
- fault injection is opt-in per Engine: an env-triggered `os.kill(SIGKILL)` in
  production code could be tripped by a stray variable. Debug prints removed.

From parallel TDD workstreams:
- context: summary spans carry an explicit coordinate frame; the citation
  round-trip went 80% -> 100%. Summary ids digest the full child list (two
  sibling sets could silently overwrite each other). Truncation is announced.
- review: channel failure no longer becomes `pass_with_risk`; `max_rounds` is
  honoured; the frozen ledger actually constrains review; whitespace is not
  evidence; author/reviewer session separation is asserted.
- benchmarks: the GO/NO-GO verdict skipped its first gate row. Seeded-defect
  "repair" was a string-level inverse of the seeder -- rewritten to classify
  structurally with `ast`, and held-out spellings are now detected. One seeded
  fixture was not even defective.
- metrics: corrected branching is derived from `admission_checked` events rather
  than planner labels; unmeasurable ratios report None instead of a fabricated
  0.0; `statistic="median"` used an order statistic, not the median.

Tests: 121 -> 438 passing. ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
#493)

- `output_schema` had 9 declaration sites and 0 readers; `run_capability` now
  validates both input and output against the declared schema and raises
  `CapabilityContractError` rather than propagating an off-contract value.
- the root plan emits a `decompose_outcome`: it is itself a decomposition of the
  goal, and without it every admission on a root step was a correction
  attributed to no measured decomposition.
- benchmark defect classification is derived from the event log instead of
  planner instance state. A solution-cache hit reuses a plan without calling the
  planner, so detection read as 3/16 while every repair was in fact correct and
  externally verified. Cached and freshly-authored plans are now measured alike.
- scenarios start from a clean workspace. They opened SQLite in place, so
  re-running into an existing output directory re-indexed every chunk; the
  duplicates diluted top-k and needle recall silently fell 1.0 -> 0.625. A
  benchmark that degrades the more often you run it is not a measurement.

Regenerated artifacts, all eight gates PASS (GO). Two honest caveats now visible
in the numbers rather than hidden by them:
- `total_tokens: None`, `runs_with_token_measurements: 0` — hermetic runs consult
  no model, and that is now reported as unmeasured instead of a fabricated 0.0.
- all 41 `m_values` are 0.0. This is a genuine measurement, not the previous
  structural blindness: the fixture distribution simply contains no ambiguous
  children, so it does not yet exercise the branching the gate is meant to bound.

Tests: 439 passing. ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
…lay (#493)

Docs (every claim re-verified by execution; corrected where false):
- `validate_plan` does NOT check authority — containment is enforced in
  kernel/capabilities/admission via `sherpa.authority`. Attribution fixed.
- `replay_projection` rebuilds status, nodes (state/owner_session/depth/
  parent_key), usage, pending messages and finding ids — but NOT chunks,
  summaries or the solution cache. Stated precisely instead of "everything".
- negative solution-cache results are still NOT written: recorded as a known gap
  rather than listed as encoded.
- budget exhaustion terminates `budget_exhausted` and writes no cache entry; the
  "recorded as inconclusive" claim was false.
- the walkthrough excerpt is now generated from the transcript rather than
  retyped, and the reproducibility claim names the two lines that legitimately
  differ between runs (temp path, run id) instead of implying byte-identity.
- new Authority and Limitations sections.

Code:
- `_parse_unified_diff` filed each file's hunks under the NEXT file, because the
  pending hunk was flushed only after `current_file` had advanced. With two files
  sharing context lines it wrote both edits into one file and reported success
  with no exception. Real multi-file `git diff` output was also unparseable, and
  `@@ -0,0 +1,N @@` could never create a file.
- `RecordedChannel` was keyed on session alone — positional FIFO, not replay, so
  an unrecorded prompt silently received the next queued answer. Entries may now
  be request-keyed and a mismatch raises `RecordingMismatch`. The legacy
  `{"session": ["text"]}` form still works.
- `max_attempts_per_node` was declared and enforced nowhere; a node past the cap
  now terminates `budget_exhausted` instead of retrying indefinitely.
- a non-final `else` branch case is rejected: the guard made the check
  unreachable whenever the last case was also an else, so a leading else silently
  turned every later case into dead code.

Correction to my own brief: `if not branch` is NOT dead code — `min_length=1`
constrains only the outer list, so `Parallel(branches=[[]])` constructs. Kept and
pinned by a test.

Measured max recursion depth across all 41 benchmark run databases: 1.

Tests: 439 -> 461 passing. ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
@jeremymanning

Copy link
Copy Markdown
Member Author

Audit and correctness pass

This branch started as a draft MVP that had not been thoroughly checked. It has now had a full audit — seven parallel deep reviews, with every critical finding independently reproduced by execution against a clean tree — followed by a correctness pass under strict TDD (failing test first, watched fail, then fixed).

Tests: 121 → 461 passing. ruff clean. All eight preregistered #492 gates PASS.

What the audit found

The event-sourced kernel, admission pipeline and bounded review gate were a genuinely good substrate. But the branch did not satisfy the gates it claimed. The load-bearing findings:

Authority enforcement was non-functional. Three divergent matchers answered the same question differently, and the one used as the gate reduced to candidate.startswith("") for any grant ending in * — so every wildcard grant allowed everything. Nothing in the package normalized a filesystem path (grep -E 'resolve\(\)|is_relative_to|realpath|commonpath' over src/sherpa returned zero matches). Demonstrated end-to-end with grant src/**: reading an arbitrary absolute path outside the workspace, following a symlink out, and overwriting a file outside the workspace all succeeded. Separately, repo.run_tests never authority-checked its cwd (pytest executes conftest.py from it), and repo.apply_patch wrote the file before its containment check.

Exactly-once was not guaranteed. The lease worked; the kernel ignored it — _begin_attempt returned True when acquisition failed, so a second worker executed a node another live session held.

The central viability metric measured nothing. m_corrected counted planner self-reports and never read admission_checked; and decompose_outcome was emitted only when the parent was not skipped, which is exactly what the reclassify path sets — so every admission correction was structurally invisible. All 41 m_values were 0.0. The GO/NO-GO verdict also skipped its own first gate row (lines[3:] where lines[2] is the first gate), so the headline gate could never produce NO-GO. And the seeded-defect "repair" was a string-level inverse of the seeder: semantically identical defects from the same declared grammar were rejected as "outside preregistered grammar".

Recursion depth was never tracked. depth was a local pinned at 0 and _author_child's depth argument was dead, so max_depth bounded only structural nesting inside one plan — recursive decomposition was unbounded. parent_key and owner_session were NULL for every node even after a real decomposition, so no organization tree was persisted at all.

What changed

  • sherpa.authority — one implementation, shared by delegation, admission and execution. Delegation (pattern subset) is separated from access (resolved-resource containment); paths are resolved before comparison so .. and symlinks are visible to the check. ** is workspace-relative; /** is the only way to ask for the whole filesystem. CapabilitySpec.requires names authority dimensions, so scoped fs grants work — previously any scoped grant made every fs capability unusable, leaving grant-everything as the only working configuration.
  • Exactly-once — lease refusals are honoured and recorded (lease_denied).
  • Org tree — depth and parent travel on the execution stack and are persisted per node, and replay_projection rebuilds them.
  • Loud terminals — planner failures and plan-gate refusals were escaping Engine.run, stranding runs in running with no run_terminal. They are terminal states now.
  • Honest measurement — corrected branching derives from admission_checked; unmeasurable ratios report None instead of a fabricated 0.0; typed I/O is enforced on every invocation (output_schema previously had 9 declarations and 0 readers); model spend is recorded so token/cost budgets can bind.
  • Review gate — a channel failure no longer becomes pass_with_risk; max_rounds is honoured; the frozen concern ledger actually constrains review; author/reviewer separation is asserted.
  • Context — summary spans carry an explicit coordinate frame; the citation round-trip went 80% → 100%.
  • Safety — the env-triggered os.kill(SIGKILL) is now opt-in per Engine; capability inputs are redacted in the event log.

Evidence the fixes bind

Mutation testing of the new code — each mutation applied alone, tree verified clean after each:

mutation result
path containment always allows 37 tests fail
delegation check always allows 8 tests fail
stop resolving paths (traversal/symlink) 10 tests fail
skip the per-path fs check 6 tests fail
blank owner_session on transition 1 test fails
ignore the lease refusal 1 test fails

Honest limitations, stated rather than hidden

  • All 41 m_values are 0.0. This is now a genuine measurement rather than structural blindness — but the fixture distribution contains no ambiguous children, so it does not yet exercise the branching the gate exists to bound.
  • total_tokens: None, runs_with_token_measurements: 0 — hermetic runs consult no model. Reported as unmeasured, not as a measured zero.
  • Issue yet another redesign #485's Component II (organization tree) is only partially realized: parent/child linkage and depth are persisted, but there is no per-node agent identity and no message routing (delivery is exact-key, addressed by the caller).
  • yet another redesign #485's Component III (shared scratchpad of agent thinking, red-teamed insights pool, shared tool pool) is not implemented.

Both are follow-up work, tracked separately.

The lease fix stopped CONCURRENT double execution. It did not close the crash
window between a side effect reaching disk and its completion event: on resume
the kernel replayed the attempt blindly. The audit reproduced this with an
append capability, which resumed to ['one', 'one', 'two'] while reporting
`completed`.

#492 promises only that "completed IDEMPOTENT nodes are not repeated", so the
runtime has to be able to tell the two apart — and nothing declared it.

- `CapabilitySpec.idempotent`, defaulting to False: an undeclared capability is
  assumed unsafe to repeat.
- `Engine._attempt_in_doubt` detects an unmatched `tool_call_started`, which is
  exactly that crash window.
- On resume an in-doubt attempt is replayed only if the capability declares
  itself idempotent; otherwise the run escalates loudly instead of duplicating
  work.
- Built-ins declare it: reads and same-bytes writes are idempotent,
  `repo.apply_patch` is not.

The interrupted-capability test fixture now declares `idempotent=True`, which is
truthful — it raises before doing anything — and is required for the attempt
budget it exercises to be reachable at all.

Mutation check: treating every capability as idempotent turns the suite red.

Tests: 461 -> 463 passing. ruff clean. CI green on py3.11/3.12/3.13 x {ubuntu, macos}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmJGGdtCgwTVspskkLorYk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MVP: evidence-driven recursive agentic runtime

1 participant