Status: Drafted 2026-05-22 alongside token-reduction-strategy.md §5.1.1.7.
Purpose: Five drop-in agent prompts for parallel execution of Wave 18.
Phase structure: 3 agents in Phase 1, 2 agents in Phase 2 (see §5.1.1.7 of the umbrella).
How to use this file. Each
## Agent Xsection below is a verbatim prompt. Copy it whole, paste into a fresh Claude Code session (or pass to theAgenttool withisolation: "worktree"), and wait for the agent to halt at PR. Do not merge until you have reviewed.
Every agent prompt below assumes the agent will:
origin/main, not local HEAD:
git fetch origin main
git checkout -b <branch> origin/main
uv run pytest
uv run ruff check packages scripts
uv run ruff format packages scripts
Branch: wave-18a-1
Size: L (~3–5 sittings of focused work)
Phase: 1
Dependencies: none
Unblocks: Phase 2 (Agents D, E)
docs/specs/provider-adapter-contract.md §4.6 — the contract you are implementing. §4.6.1 through §4.6.7 inclusive.docs/specs/provider-adapter-contract.md §4.5 — the OpenRouter prompt-caching section is the closest existing analog for §4.6 work; mimic its style.docs/design/token-reduction-strategy.md §5.1.1.2 → 18a-1 — acceptance criteria.packages/metis/src/metis/core/adapters/anthropic.py (current sync adapter — your batch additions live alongside it, NOT as a new file).Modify:
packages/metis/src/metis/core/canonical/capabilities.py — AdapterCapabilities: add supports_batch_api: bool = False.packages/metis/src/metis/core/canonical/messages.py — Usage: add pricing_mode: Literal["sync", "batch"] | None = None. Update the disjoint-bucket docstring to mention the new field is orthogonal to the three input-token buckets.packages/metis/src/metis/core/pricing/table.py — ModelPricing: add batch_rates: ModelPricing | None = None. When set, batch-mode calls cost off this row instead of the sync row.packages/metis/src/metis/core/adapters/protocol.py — add three methods to ProviderAdapter Protocol: submit_batch, fetch_batch, poll_batch. Each has a default implementation in the base that raises NotImplementedError so existing OpenAI / OpenRouter adapters still satisfy the Protocol without code changes.packages/metis/src/metis/core/adapters/anthropic.py — implement the three methods against POST /v1/messages/batches, GET /v1/messages/batches/{id}, GET /v1/messages/batches/{id}/results. Declare supports_batch_api=True for at least the anthropic:claude-haiku-4-5 row in the model registry.Create:
packages/metis/src/metis/core/canonical/batch.py (new file) — house the new types: BatchHandle, BatchStatus (literal), BatchResult (union), BatchError. Keep them in msgspec.Struct(frozen=True) style consistent with the rest of canonical/. Re-export from canonical/__init__.py.packages/metis/tests/core/adapters/test_anthropic_batch.py (new file) — SDK-mock-driven round-trip (SimpleNamespace-based mocks of the anthropic SDK’s messages.batches.{create,retrieve,results}, matching the repo convention in test_anthropic_adapter.py). No VCR cassettes.CanonicalRequests, polls poll_batch until BatchStatus="completed", fetches via fetch_batch. The returned list is same length and same order as the input, with custom_ids preserved.anthropic:* model row in the registry declares supports_batch_api=True.Usage.pricing_mode="batch" is stamped on every successful result.Usage.cost_usd matches ModelPricing.batch_rates (50% of sync) when present; when absent, the adapter logs a single WARN line and falls back to sync rates (correctness preserved, savings lost).BatchError(error_class=ErrorClass.SERVER_ERROR, retryable=True) per custom_id. (ErrorClass is a closed enum; SERVER_ERROR is the retryable bucket — there is no PROVIDER_TRANSIENT value.)packages/metis/tests/core/adapters/ pass unchanged.uv run mypy packages/metis/src/metis/core clean.test_anthropic_batch.py.BaseHTTPMiddleware-style wrappers. Batch adapter code goes through the standard httpx.AsyncClient path; no middleware involved.SimpleNamespace-shaped mocks of the anthropic SDK surface (see test_anthropic_adapter.py); do not introduce VCR cassettes.Usage.pricing_mode is Optional. Pre-§4.6 trace rows have NULL; analytics consumers (out of scope for 18a-1) will treat NULL as "sync". Your job is to stamp "batch" correctly; don’t worry about back-compat queries.BatchError, not raises. The list returned by fetch_batch is list[BatchResult] where BatchResult = CanonicalResponse | BatchError. Only batch-level failures (entire batch failed before any results) raise AdapterError.metis evaluate --batch-mode (18a-2) and scripts/benchmark.py --batch-mode (18a-3) consume your adapter additions in Phase 2 — they are out of scope for this PR.BatchHandle etc. to canonical/__init__.py so callers can from metis.core.canonical import BatchHandle consistently with the rest of canonical/.uv run pytest # full suite passes
uv run ruff check packages scripts # lint clean
uv run ruff format packages scripts # auto-format
git add -A
git commit -m "feat(adapters): Anthropic batch submission (Wave 18a-1)
Implements docs/specs/provider-adapter-contract.md §4.6 for the
Anthropic adapter. New canonical types (BatchHandle, BatchStatus,
BatchResult, BatchError) + Usage.pricing_mode + ModelPricing.batch_rates
+ three new ProviderAdapter Protocol methods (default
NotImplementedError) + Anthropic implementation against
/v1/messages/batches. Unblocks Wave 18a-2 / 18a-3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
git push -u origin wave-18a-1
gh pr create --title "feat(adapters): Anthropic batch submission (Wave 18a-1)" \
--base main \
--body "Implements [provider-adapter-contract.md §4.6](docs/specs/provider-adapter-contract.md) per the Wave-18 dispatch plan in [token-reduction-strategy.md §5.1.1.2](docs/design/token-reduction-strategy.md). Unblocks Wave 18a-2 / 18a-3.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
HALT — do not start any other sub-item.
Branch: wave-18a-4
Size: S (~1–2 sittings)
Phase: 1
Dependencies: none
Unblocks: Wave 19 §19a-2
docs/specs/session-compaction.md §5 — full storage section (§5.1 / §5.2 / §5.3 / §5.4) is the spec for this sub-item.docs/specs/pattern-store.md §17 — concurrency pattern (threading.RLock()) you are reusing.packages/metis/src/metis/core/sessions/sqlite_store.py — closest analog for the SQLite-store-in-sessions pattern.Create:
packages/metis/src/metis/core/sessions/compaction_cache.py (new file) — class CompactionCache with the API per session-compaction.md §5.2:
class CompactionCache:
def __init__(self, path: Path, *, max_rows: int = 1000) -> None: ...
def read(self, cache_key: str) -> CompactionRow | None: ...
def write(self, cache_key: str, summary_text: str, *,
summarization_model: str,
summarization_prompt_version: str,
span_message_count: int,
span_token_count_in: int,
span_token_count_out: int) -> None: ...
def evict_lru(self) -> int: ...
def close(self) -> None: ...
packages/metis/tests/core/sessions/test_compaction_cache.py (new file).from metis.core.sessions.compaction_cache import CompactionCache.CompactionCache(path) creates the SQLite file at path with the schema in session-compaction.md §5.2 (exact columns, exact PRIMARY KEY, exact index).write() then read() round-trips a row; second read() updates last_read_at_ms + increments use_count.evict_lru() keeps at most max_rows rows, evicting the one with the oldest last_read_at_ms first.threading.RLock() wraps every public method (mimic pattern-store.md §17).sqlite3.InterfaceError.SessionManager does not call CompactionCache in this PR. The store exists but is unused. Wave 19 §19a-2 will wire it.test_compaction_cache.py.numpy dependency. session-compaction.md §5.2 doesn’t need vector storage; keep dependencies minimal.pattern-store.md §17 and trace-store’s wal_autocheckpoint). Don’t roll your own pragma config — copy the pattern.path argument is the file path, not a workspace path. The caller (Wave 19) will pass <workspace>/.metis/compaction-cache.sqlite; this sub-item is path-agnostic.session.compaction_* events. Agent C (18a-5) defines those event payloads; the emitter is wired by Wave 19 §19a-2.uv run pytest packages/metis/tests/core/sessions/ -x
uv run pytest # full suite
uv run ruff check packages scripts
uv run ruff format packages scripts
git add -A
git commit -m "feat(sessions): CompactionCache SQLite store (Wave 18a-4)
Schema-only SQLite store for the rolling-summary cache per
session-compaction.md §5. threading.RLock concurrency; LRU eviction;
1000-row default cap. No caller wiring — Wave 19 §19a-2 lights it up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
git push -u origin wave-18a-4
gh pr create --title "feat(sessions): CompactionCache SQLite store (Wave 18a-4)" \
--base main \
--body "Implements [session-compaction.md §5](docs/specs/session-compaction.md) per the Wave-18 dispatch plan in [token-reduction-strategy.md §5.1.1.2](docs/design/token-reduction-strategy.md). Schema only; no caller wiring (Wave 19 §19a-2 will wire SessionManager).
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
HALT — do not start any other sub-item.
session.compaction_* events in catalogBranch: wave-18a-5
Size: XS (~1 sitting)
Phase: 1
Dependencies: none
Unblocks: Wave 19 §19a-2
docs/specs/session-compaction.md §6 — the payload schemas you are registering.docs/specs/event-bus-and-trace-catalog.md §6 — where the new rows land. Look at the session.* family entries for shape.packages/metis/src/metis/core/events/payloads.py — the PAYLOAD_REGISTRY (last ~80 lines) and existing session.* payload structs for style.Modify:
packages/metis/src/metis/core/events/payloads.py — three new msgspec.Struct(frozen=True) payload classes per session-compaction.md §6:
SessionCompactionStarted — {session_id, turn_id, watermark_before, span_message_count, span_token_count_in, threshold_or_hard_cap}SessionCompactionCompleted — {session_id, turn_id, watermark_before, watermark_after, span_message_count, span_token_count_in, span_token_count_out, cache_hit: bool, cache_key, cost_usd: Decimal, latency_ms}SessionCompactionFailed — {session_id, turn_id, watermark_before, span_message_count, failure_mode: Literal["adapter_error", "no_valid_boundary", "budget_exhausted_forced", "validation_failed"], error_class: ErrorClass | None, error_message: str | None, truncated_message_count: int | None}PAYLOAD_REGISTRY with Sensitivity.PSEUDONYMOUS for all three.packages/metis/tests/core/events/test_payloads.py — round-trip tests for each new payload + catalog-membership tests.docs/specs/event-bus-and-trace-catalog.md §6 — three new rows under the session.* family. Mirror the style of the existing session.created / session.resumed / session.ended entries.PAYLOAD_REGISTRY["session.compaction_started"] resolves to (SessionCompactionStarted, Sensitivity.PSEUDONYMOUS).session.compaction_completed and session.compaction_failed.make_event(type=..., payload=struct) followed by JSON encode + decode produces the same struct.event-bus-and-trace-catalog.md §6 carries three new payload rows with full schemas.SessionManager produces these events in this PR. Wave 19 §19a-2 wires the emitter.docs/specs/CHANGES.md — the existing 2026-05-22 — session-compaction.md v1 entry’s “References to verify” note about event-bus-and-trace-catalog.md §6 should move from pending review toward verified; add a one-line update under the existing entry naming this sub-item’s branch / PR.PSEUDONYMOUS, NOT PRIVATE. Summary text lives in the message store, not the event payload, so PRIVATE-tier sensitivity is wrong. PSEUDONYMOUS is correct.AUDIT_EVENT_TYPES. Compaction is an operational optimization, not a compliance event. Do not add these to the audit frozenset.Decimal for cost_usd, not float. Match existing LLMCallCompleted style.failure_mode literal must include exactly the four values in session-compaction.md §6: "adapter_error" | "no_valid_boundary" | "budget_exhausted_forced" | "validation_failed". Don’t add or remove.uv run pytest packages/metis/tests/core/events/ -x
uv run pytest # full suite
uv run ruff check packages scripts
uv run ruff format packages scripts
git add -A
git commit -m "feat(events): session.compaction_* in catalog (Wave 18a-5)
Three new PSEUDONYMOUS event payloads + PAYLOAD_REGISTRY entries per
session-compaction.md §6. Spec docs/specs/event-bus-and-trace-catalog.md
§6 carries the new rows. No emitter wiring — Wave 19 §19a-2 wires
SessionManager.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
git push -u origin wave-18a-5
gh pr create --title "feat(events): session.compaction_* in catalog (Wave 18a-5)" \
--base main \
--body "Implements [session-compaction.md §6](docs/specs/session-compaction.md) per the Wave-18 dispatch plan in [token-reduction-strategy.md §5.1.1.2](docs/design/token-reduction-strategy.md). Catalog wiring only; emitter lands Wave 19 §19a-2.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
HALT — do not start any other sub-item.
Prerequisite check before spawning Phase 2: verify
wave-18a-1has merged toorigin/main. If not, spawning these agents is wasted work — they will fail at the test step.git fetch origin main git log origin/main --oneline -5 # expect to see the 18a-1 PR merge commit
metis evaluate --batch-modeBranch: wave-18a-2
Size: M (~2 sittings)
Phase: 2
Dependencies: 18a-1 merged
docs/specs/provider-adapter-contract.md §4.6 — the adapter contract 18a-1 implemented.docs/specs/evaluator.md — the existing evaluator surface, especially §6.2 (the metis evaluate CLI).docs/design/token-reduction-strategy.md §5.1.1.2 → 18a-2.packages/metis/src/metis/core/eval/cli.py — the current evaluator CLI entry.packages/metis/src/metis/cli/main.py — where the evaluate subparser registers (around line 76).Modify:
packages/metis/src/metis/cli/main.py — add --batch-mode (submit-only) and --collect-batches (poll + ingest) flags to the existing evaluate subparser.packages/metis/src/metis/core/eval/cli.py — branch on the new flags:
--batch-mode: collect all eval requests for the window, call adapter.submit_batch, persist the handle, exit without waiting.--collect-batches: read pending handles, call adapter.poll_batch; for each completed batch, call adapter.fetch_batch and emit eval.completed events with pricing_mode="batch" stamped (via the adapter’s Usage.pricing_mode already from 18a-1).packages/metis/src/metis/core/eval/subscriber.py — if any of the eval.completed emission paths assume sync-mode behavior, surface them and add the batch path.docs/specs/evaluator.md — new subsection under §6.2 documenting the two flags + the two-pass workflow.Create:
packages/metis/tests/core/eval/test_batch_mode.py (new file) — submit → poll → fetch → ingest cycle against a fixture; idempotency on a second --collect-batches.Touches the trace DB (additive):
evaluator_batch_handles in the existing trace DB. Schema (additive; do NOT bump TRACE_SCHEMA_VERSION):
CREATE TABLE IF NOT EXISTS evaluator_batch_handles (
custom_id TEXT PRIMARY KEY,
batch_id TEXT NOT NULL,
provider TEXT NOT NULL,
submitted_at_ms INTEGER NOT NULL,
subject_kind TEXT NOT NULL,
subject_id TEXT NOT NULL,
status TEXT NOT NULL,
ingested_at_ms INTEGER
);
CREATE INDEX IF NOT EXISTS idx_evaluator_batch_handles_status
ON evaluator_batch_handles(status);
metis evaluate --batch-mode --subject turn --since 2026-05-01T00:00:00Z (against a known trace DB) submits a batch, persists handles in evaluator_batch_handles, exits without waiting. STDOUT prints the batch id(s) + an informational line.metis evaluate --collect-batches polls pending handles, ingests completed results as eval.completed events, marks evaluator_batch_handles.status = "ingested" and stamps ingested_at_ms.tests/fixtures/eval_batch_fixture.db or similar) match byte-for-byte against the sync metis evaluate output for the same window. The only difference is eval.completed.payload.pricing_mode = "batch".--collect-batches invocation is idempotent — no duplicate eval.completed events.TRACE_SCHEMA_VERSION — the new table is additive. CREATE TABLE IF NOT EXISTS makes the migration zero-friction.subject_kind = "turn" | "session" | "workload" — the spec already supports re-evaluation at three subject kinds via §6.2; preserve all three.--subject per invocation, NOT one batch per turn. A single evaluator.submit_batch call should bundle all in-window items, up to the Anthropic 100k / 256MB cap. If the window exceeds the cap, chunk into multiple batches and persist multiple rows in one invocation.pricing_mode="batch" propagation. The adapter’s Usage already carries it from 18a-1. Your job is to make sure the eval.completed event payload (or the LLMCallCompleted it triggers) preserves it.--batch-mode + --collect-batches. Don’t add --cancel-batch, --list-batches, etc.; out of scope.uv run pytest # full suite
uv run ruff check packages scripts
uv run ruff format packages scripts
git add -A
git commit -m "feat(eval): metis evaluate --batch-mode (Wave 18a-2)
Submits eval re-runs to the Anthropic Batches API at 50% discount;
persists handles in a new evaluator_batch_handles table;
--collect-batches polls + ingests. Verdicts match sync mode
byte-for-byte (only pricing_mode differs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
git push -u origin wave-18a-2
gh pr create --title "feat(eval): metis evaluate --batch-mode (Wave 18a-2)" \
--base main \
--body "Captures the 50% Anthropic Batches API discount on evaluator backfills per [token-reduction-strategy.md §4 Tier 2](docs/design/token-reduction-strategy.md) and [provider-adapter-contract.md §4.6](docs/specs/provider-adapter-contract.md). Depends on Wave 18a-1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
HALT — do not start any other sub-item.
scripts/benchmark.py --batch-modeBranch: wave-18a-3
Size: M (~2 sittings)
Phase: 2
Dependencies: 18a-1 merged
docs/specs/provider-adapter-contract.md §4.6 — the adapter contract 18a-1 implemented.docs/specs/benchmark.md — the existing benchmark spec; identify where batch fits.docs/design/token-reduction-strategy.md §5.1.1.2 → 18a-3.scripts/benchmark.py — the current harness.benchmarks/RESULTS.md — the format for the new entry you’ll add.Modify:
scripts/benchmark.py — add --batch-mode (submit only) and --collect-batch <run_id> (poll + ingest) flags. Sub-shape mirrors Agent D’s evaluator pattern.docs/specs/benchmark.md — new subsection documenting the two flags and the two-pass workflow.benchmarks/RESULTS.md — new §Wave-18 entry with measured cost-per-quality reduction on at least one workload.Persistence (new artifact directory pattern — no code-level new files):
benchmarks/.runs/<run_id>/batch-handles.jsonl (gitignored under .runs/) — one JSONL line per submitted batch: {custom_id, batch_id, provider, submitted_at_ms, workload, model, status}.scripts/benchmark.py --batch-mode --workload fix-a-bug-small submits the workload’s turn requests as a batch, writes benchmarks/.runs/<run_id>/batch-handles.jsonl, exits without waiting.scripts/benchmark.py --collect-batch <run_id> polls pending handles, finalizes the run, produces the standard benchmarks/.runs/<run_id>/results.json (or whatever the existing harness emits) with pricing_mode="batch" on every LLMCallCompleted.benchmarks/RESULTS.md §Wave-18 entry documents:
temperature is unchanged).scripts/ and may not have full pytest coverage — add tests where the existing harness has them; otherwise document smoke results inline).--collect-batch takes a <run_id> — the harness identifies runs by id already, so this flag re-uses the existing run id from --batch-mode’s output.benchmarks/.runs/ is gitignored. Don’t check in the JSONL handles file.RESULTS.md entry is the load-bearing artifact for this sub-item. Without a measurable cost-per-quality entry, the wave doesn’t demonstrate the 50% reduction; spend the time to make the comparison clean.uv run pytest # full suite
uv run ruff check packages scripts
uv run ruff format packages scripts
# Live API spend on a small workload to populate RESULTS.md:
uv run python scripts/benchmark.py --batch-mode --workload fix-a-bug-small
# wait, then:
uv run python scripts/benchmark.py --collect-batch <run_id>
# document results in benchmarks/RESULTS.md
git add -A
git commit -m "feat(benchmark): scripts/benchmark.py --batch-mode (Wave 18a-3)
Submits benchmark runs to the Anthropic Batches API at 50% discount;
persists handles per-run; --collect-batch finalizes. RESULTS.md §Wave-18
documents cost-per-quality on fix-a-bug-small.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
git push -u origin wave-18a-3
gh pr create --title "feat(benchmark): scripts/benchmark.py --batch-mode (Wave 18a-3)" \
--base main \
--body "Captures the 50% Anthropic Batches API discount on benchmark re-runs per [token-reduction-strategy.md §4 Tier 2](docs/design/token-reduction-strategy.md) and [provider-adapter-contract.md §4.6](docs/specs/provider-adapter-contract.md). Depends on Wave 18a-1.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
HALT — Wave 18 closes once this and 18a-2 are merged.
Once all five PRs (wave-18a-1 through wave-18a-5) are merged to
origin/main, run the §5.1.1.4 doc-sync checklist from
token-reduction-strategy.md:
[ ] AGENTS.md status sentence: "Wave 18 reaches the
batch-API + compaction-substrate milestone."
[ ] AGENTS.md "What works" — one entry for async batch (18a-1/2/3),
one for compaction substrate (18a-4/5).
[ ] README.md — test count bumped 1865 → ~1909.
[ ] docs/specs/CHANGES.md — two `pending review` entries
(`session-compaction.md` + `provider-adapter-contract.md §4.6`)
move to `verified`.
[ ] docs/specs/event-bus-and-trace-catalog.md §6 — three new
`session.compaction_*` rows landed via 18a-5.
[ ] docs/specs/evaluator.md — `--batch-mode` documented via 18a-2.
[ ] docs/specs/benchmark.md — `--batch-mode` documented via 18a-3.
[ ] benchmarks/RESULTS.md — §Wave-18 batch-mode measurement landed
via 18a-3.
Then Wave 19 planning starts; see token-reduction-strategy.md §5.2.