CE-13 (Story-2071) built the assembler: one context_bundle.v1
(cards → chronology → evidence → assertions → contradictions), budgeted, hashed,
cached in context.bundles, rebuilt by the event-debounced worker.
CE-14 puts that bundle on the wire: the broker pre-fetches the
pinned bundle at launch and stamps it onto agent_invoke.v1, beside
the Memory Pack, in the ONE pre-fetch assembly (RFD
docs/notes/rfd-context-engine.md §5.4/§5.5; the shared seam of
docs/notes/rfd-memory-engine.md §12.2 R-6).
A brain's only egress is the inference provider (pramaan-docs ADR-110); it cannot
query cards, retrieval, the Neptune graph, and assertions while it reasons. So the
broker must hand it one bounded, versioned pack at launch. And the command panel
spawns four roles far-side (proposer×2, critic, synthesizer). If each role
fetched its own bundle they could see different context — the synthesizer
would judge a draft built against a bundle it never saw, and "what did the model
read" forks. CE-14 reads ONE bundle at launch and pins it: every role reads exactly
this version_hash.
| Surface | Change |
|---|---|
src/shared/agent_contracts/invoke.py |
New additive optional field AgentInvoke.context_bundle:
ContextBundle | None, mirroring memory_pack. A missing
bundle is valid (fail-open); the brain behaves as today. context_contracts
is sqlalchemy-free (CI-checked), so importing ContextBundle keeps
the envelope importable by the brains with no DB dependency. |
src/shared/context/bundle_prefetch.py (new) |
prefetch_context_bundle(session, matter_id, kind) — a
cache READ, never a build. Reads the pinned
context.bundles row for the run's (profile, budget),
chosen by turn kind (§5.5): mira_command → strategy×L
(proposers get L breadth), dock_chat → chat×S (converse, frame
primary). A miss returns None. Bundles are (re)built by the
debounced worker (§9); building inline would blow the Lambda round-trip budget
and DR-1. |
src/clerk_worker/runner.py — _stamp_prefetch_packs() |
The one assembly point (R-6 / CE-14 seam). ME-7 built this
function to stamp command["memory_pack"]; CE-14 adds
command["context_bundle"] right beside it — both additive optional
fields on the same fail-open pre-fetch, neither reworks the other. Scoped to a
matter (a bundle needs a matter_id); a form-only or unscoped launch stamps no
bundle. |
src/clerk_worker/runner.py — _invoke_brain() |
Rides the stamped bundle onto the envelope:
if command.get("context_bundle") is not None:
envelope_dict["context_bundle"] = command["context_bundle"] — identical
shape to the memory_pack ride. |
system + role charter
→ current_frame @ frame_version # frame lives OUTSIDE the bundle
→ memory block (memory_pack.v1) # ME-6/ME-7
→ bundle @ version_hash (context_bundle.v1) # CE-14 — this story
→ per-turn suffix (injection nonces stay suffix-only)
The frame is carried once, outside the bundle and outside its hash (collision #6),
so the cache key is (prompt_version, frame_version, version_hash). The
base bundle is delimited with a version_hash-stable delimiter; per-run
injection nonces stay in the per-turn suffix. The far-side rendering of this
sandwich (per-role budgets: critic S, synthesizer verbatim) is the agents rail's
CE-15/CE-16 — CE-14 pins the one base bundle they share.
process_command
└─ (remember? → short-circuit, no brain) # ME-7
└─ _inject_granted_tool_catalog(command, deps) # dock_chat tool catalog
└─ _stamp_prefetch_packs(command, deps) # ← THE seam
├─ command["memory_pack"] = assemble_memory_pack(...) # ME-7
└─ command["context_bundle"] = prefetch_context_bundle(...) # CE-14 (this story)
└─ _invoke_brain → envelope_dict["memory_pack"] = command["memory_pack"]
envelope_dict["context_bundle"] = command["context_bundle"]
A seeded fixture hides the case that breaks first. The integration test
(tests/integration/test_bundle_prefetch.py) drives
prefetch_context_bundle against a real Postgres as
pramaan_app:
None → the invoke carries no
context_bundle and the brain behaves as today (fail-open). The door
is open, not welded shut.kind='mira_command' return that same version_hash — the
AC "one version_hash across all roles of a pinned run".kind='dock_chat' reads chat×S
(a different, unbuilt combo) → a miss, proving the kind→axes gate hits the right
cache row.
The kind→axes decision is also locked as a pure unit test
(tests/unit/context/test_bundle_prefetch.py): an unmapped kind
(intake) pins nothing WITHOUT touching the session — a stray query on
intake is a hard failure.
Per-role prompt-sandwich rendering (proposer/critic/synthesizer
budgets, the delimiters) is the agents rail (CE-15/CE-16) — CE-14 pins the shared
base bundle. Persisting a context_provenance row needs
its own store (a migration this no-migration story does not carry); the honesty
record rides the consumer's turn.completed payload (§10), emitted
far-side. Building on a cache miss is deliberately NOT done here —
that is the debounced rebuild worker's job (§9); the broker only reads, keeping the
invoke path DR-1-bounded.
uv run ruff check
uv run pytest tests/unit/context/test_bundle_prefetch.py \
tests/integration/test_bundle_prefetch.py \
tests/unit/agent_contracts # envelope additive-diff
make openapi # app/admin snapshots unchanged (agent_invoke.v1 is not an HTTP DTO)
Recall: CE-14 pre-fetches the pinned context_bundle.v1
and stamps it onto agent_invoke.v1 beside the Memory Pack in the one
fail-open broker assembly, so every panel role reads exactly one
version_hash — the bundle rendered after the memory block and after the
frame, which stays outside the bundle.