surface_context stamping
Wave B of the founder's viewer plan: "Mira always knows which page
counsel is on." CE-14 (Story-2072) put a pinned ContextBundle
on agent_invoke.v1 at launch. Story-2152 adds — strictly
additively — the one fact that bundle didn't carry: the surface
counsel is looking at. When that surface names a document, the same
pinned bundle is scoped document-first, and the surface rides to
the runtime so Mira's prompt can say "Counsel is viewing:
HARSHA_VENKATESH_PLAIN.pdf."
A matter's pinned bundle is matter-wide: every document's cards, chunks, and assertions, budgeted and ordered by the builder. But counsel is not asking about the whole matter — they are on one page, reading one document, and their question ("what does this say?", "is this consistent?") is about that document. If the bundle leads with matter-wide content, the document counsel is staring at can fall past the budget's most-attended region (lost-in-the-middle), and Mira answers as if it never saw the page. Wave B tells the broker which document, so that document's evidence leads.
This is orientation, never authority: document_id
only reprioritizes a read that already runs under the caller's RLS — naming a
document cannot widen what that caller may see.
| Surface | Change |
|---|---|
src/shared/agent_contracts/session_context.py |
New SurfaceContext model — { surface_key, params }.
surface_key is a durable id validated by the shared
_reject_url_like_surface_key (never a raw URL, same grammar as
SurfaceEntry); params is a typed bag
({document_id: …}). |
src/shared/agent_contracts/invoke.py |
Additive optional AgentInvoke.surface_context: SurfaceContext | None,
beside memory_pack/context_bundle. Absent → the brain
behaves as today (fail-open). No matrix rule: surface is orthogonal to
kind, so any launch may carry it. |
src/shared/agent_commands.py |
Contract-first: AgentCommand.surface_context (launch-only,
dropped from the wire when absent) so the app-side sender — a sibling story
after codegen sync — can populate it. The non-launch matrix rejects it, like
every other launch-only field. |
src/shared/context/bundle_prefetch.py |
prefetch_context_bundle(…, focus_document_id) +
scope_bundle_document_first(bundle, document_id): within every
section, items grounded in that document float to the front (stable), matter-wide
items follow. Item SET, section order, and version_hash are
unchanged — priority over the same pinned evidence, not a
rebuild. |
src/clerk_worker/runner.py |
_stamp_prefetch_packs reads surface_context.params.document_id
(defensively — a malformed value degrades to None) and passes it to
the prefetch as focus_document_id. _invoke_brain stamps
surface_context onto the envelope beside the bundle. |
The prompt sandwich is untouched: system → frame@ver → memory →
bundle@version_hash → per-turn suffix. Section order is untouched:
cards → chronology → evidence → assertions → contradictions.
Document-first reprioritizes only within each section:
cards: [docB, docA, docB] --scope(docA)--> [docA, docB, docB] evidence: [docB, docA] --scope(docA)--> [docA, docB] (via span.document_id) assertions:[docB, docA] --scope(docA)--> [docA, docB] (via spans[].document_id)
Every bundle item type carries document identity — a DocumentCard
directly, an EvidenceHit via its span, an
Assertion/ChronologyEvent/ContradictionItem
via its spans — so one helper covers all five sections.
version_hash stays the same (the pin holds)
CE-14's invariant is that every panel role of a run reads one
version_hash. The broker scopes the bundle once and
pins the scoped bundle for the whole run, so all roles still read exactly the same
bytes. Reordering is broker-side presentation over the same evidence set; it is
not a rebuild — rebuilding inline would blow the Lambda round-trip
budget (DR-1). If the model must one day see a hash that covers item order, the
upgrade path is to fold focus_document_id into the debounced builder's
hash input (noted in a ponytail: comment).
| AC | Test |
|---|---|
Invoke with surface_context(document_id) → bundle demonstrably
document-first |
test_document_named_surface_scopes_the_bundle_document_first
(drives process_command end-to-end; DOC_A is cached second, leads
the invoke's bundle once the surface names it) +
test_document_first_floats_the_named_document_within_each_section
(pure, all three item accessors). |
Invoke without surface_context → byte-identical to today
(regression) |
test_absent_surface_context_stamps_no_surface_context (envelope
surface_context is null — degrades exactly like
memory_pack/context_bundle) +
test_bundle_unchanged_when_surface_context_absent (a cached bundle
keeps its stored item order — no reshuffle). |
| Contract additive only; snapshots + permission map regenerated | test_surface_context_is_optional_and_defaults_absent,
test_surface_context_rides_additively_and_round_trips,
test_surface_context_rejects_url_shaped_surface_key;
make openapi + make gen-permissions produce no diff —
agent_invoke.v1/AgentCommand are internal broker/SQS
contracts, not HTTP DTOs (same as CE-14). |
A missing surface → nothing stamped, bundle untouched. A surface with no
document_id → the surface still rides to the runtime (Mira states the
page), the bundle is not reordered. A malformed document_id → parsed
to None defensively, no scoping. A document named that has no items in
the bundle → each section is returned untouched. Surface context is an enrichment,
never a dependency.
Closes pramaan-io/pramaan-spec#2152.