← Corpus / augment-it / spec
augment-it/specs/chat-context-awareness-architecture
- Path
- specs/Chat-Context-Awareness-Architecture.md
title: “Chat Context-Awareness Architecture — three layers (workspace as context broker, slab-assembly contract, verb registry) that grow the in-app chat from prompt-only to surface-aware verb router”
lede: “The agent-chat surface (apps/chat/) already has working scaffolding — Anthropic SDK in services/prompt-runner/src/chat-turn.ts, four cache-eligible system slabs assembled in services/workspace/src/chat.ts, three response modes (chat_answer / chat_propose / chat_invoke), the Svelte transcript + proposal-card surface. What makes it feel narrow is two design holes the v0.0.1 implementation deliberately left open: the verb roster is hand-written and prompt-only, and the context slab inlines record_set_id and nothing else. This spec defines the architecture for closing both — workspace as the single source of truth for what the user is looking at right now and what client they’re in, a slab-assembly contract that names every dimension of context the chat can use, and a verb registry that derives the model’s tool roster from CAPABILITY_TO_SUBJECT + per-verb args schemas rather than from a hand-edited prose block. The corpus-inbox /inbox verb (shipped 2026-06-08 as the first non-prompt verb) is the load-bearing demo: it works because the context slab now carries active_client_id, and it points at the seams the spec is here to formalize.”
date_created: 2026-06-08
date_modified: 2026-06-08
authors:
- Michael Staton augmented_with:
- Claude Code on Claude Opus 4.7 (1M context) semantic_version: 0.0.0.1 revisions:
- 2026-06-08 — Initial draft. Written immediately after shipping the
/inboxverb (the first non-prompt chat verb), which exposed the v0.0.1 design holes: hardcodedactive_client_id: "reach-edu"in the context slab, hand-written verb roster prose, no awareness of the operator’s current view-mode beyond record_set. This spec is the cleanup target. tags: - Spec
- Augment-It
- Chat
- Agent-Chat
- Context-Awareness
- Architecture
- Workspace
- Verb-Registry status: Draft
Chat Context-Awareness Architecture
Why this spec exists
The v0.0.1 chat surface ships with three deliberate design holes that let us land the scaffolding without committing to the architecture prematurely:
V001_CHAT_VERBSis a hand-written prose block inservices/workspace/src/chat.ts. Every new verb is a string edit in two places (the prose block + theenumon thechat_proposetool’s args schema). Tolerable at 3 verbs (the original prompt triad); brittle at 10; awful at 50.contextSlab()inlinesrecord_set_idandfocused_prompt_idonly. It does not know what view-mode the operator is in, which client they’re scoped to, what record they’re focused on, what they just did, what’s waiting for them. The chat model decides what to suggest without seeing what’s actually happening.ActiveViewis a small discriminated union (packages/workspace/src/types.ts:177) coveringidle | record_set_list | record_set | row_detail. Half the microfrontends (content-reader,inbox,pack-runner,request-reviewer,records-surface) don’t push their state into it.
The 2026-06-08 /inbox ship made all three holes concrete: the verb
roster needed a fourth verb (so the hand-written list grew), the
chat needed to know which client to write to (so we hardcoded
active_client_id: "reach-edu" in contextSlab()), and the chat
had no idea whether the operator was looking at Content Reader, the
Records Surface, or nothing — so it can’t tailor proposals to the
surface they’re on.
This spec defines the architecture for closing all three holes together. The shape is three layers, none of which need to ship at once:
- Layer 1 — workspace as context broker. Extend
ActiveView, addactive_client_id, define the contract each microfrontend honors when its state changes. - Layer 2 — slab-assembly contract. Name every dimension the chat sees, with locked semantics per dimension. Keeps the slab growth disciplined.
- Layer 3 — verb registry. Derive the model’s tool roster from
the existing
CAPABILITY_TO_SUBJECTmap plus per-verb args schemas + per-verb chat metadata (when to propose, when to invoke, what context the verb cares about).
Each layer is independently ship-able. The order below is the recommended one — Layer 1 unlocks Layer 2’s value, Layer 2 unlocks Layer 3’s payoff. But Layer 3 alone is shippable as the next thing that hurts; the others can wait.
Layer 1 — Workspace as context broker
Design
ActiveView becomes the single source of truth for what surface
the operator is in right now, with a variant per microfrontend.
The workspace singleton already has the slot
(packages/workspace/src/state.svelte.ts:39); the union shape
grows.
export type ActiveView =
| { kind: 'idle' }
| { kind: 'record_set_list' }
| { kind: 'record_set'; record_set_id: string }
| { kind: 'row_detail'; record_set_id: string; row_id: string }
// NEW — one variant per remote that has a meaningful "what am I
// looking at right now" state:
| { kind: 'content_reader'; client_id: string; record_set_id: string; focused_row_id?: string }
| { kind: 'inbox'; client_id: string }
| { kind: 'pack_runner'; record_set_id: string; focused_pack_id?: string }
| { kind: 'request_reviewer'; pending_count?: number }
| { kind: 'response_reviewer'; record_set_id: string; view_mode: 'by-response' | 'by-record' | 'content-reader' }
| { kind: 'records_surface'; record_set_id: string; focused_row_id?: string }
| { kind: 'prompt_template_manager'; focused_prompt_id?: string };
Each microfrontend takes one of two responsibilities:
- On mount and on view-state change, call
workspace.setActiveView({...})with its ownActiveViewvariant. - On unmount, IF it was the most-recently-set view, call
workspace.setActiveView({ kind: 'idle' }). This is the part that’s easy to skip; we may want a workspace-side timeout/ heartbeat as a safety net so a hung remote doesn’t leave staleactiveViewdata forever.
active_client_id — sibling top-level field
Distinct from ActiveView because the active client persists
across view changes — the operator stays in reach-edu while
hopping between Content Reader, Inbox, and Pack Runner. Put it on
the workspace singleton as a top-level field:
class Workspace {
activeView: ActiveView; // current
active_client_id: string | null; // NEW
last_capability: string | null; // current
// ...
}
Resolution rules:
- If the most-recent
ActiveViewcarries aclient_idfield, use it. - If not, fall back to a workspace-level “default client” the user
picks on first login (stored in localStorage;
'reach-edu'is the only choice today so this is degenerate). - If multiple
ActiveViewevents come from sibling federated remotes with different client_ids, the workspace logs a warning and uses the most-recent. Cross-client work in one session is a v2 question.
Capability lifecycle in the broker
Workspace already tracks last_capability (set when invoke
finishes). Extend to a small ring buffer — recent_capabilities: {capability, ok, ts}[] capped at ~10 — so the slab can inline “in
the last few minutes the user ran X, Y, Z” without exploding the
prompt. The model uses this for two things: anticipating next-verb
proposals, and grounding “what just happened?” answers.
Why the broker, not the chat surface
The chat-side chat-state.svelte.ts could in principle assemble all
of this itself by reading the workspace state at send time. The
broker pattern is more defensible because:
- Other consumers want the same info. The anticipation
suggest()function in the chat composer already readsworkspace.activeView. The status-bar badge for “N inbox items pending” reads the same. A future cross-surface “what’s the operator’s focus” indicator reads the same. - Audit and debugging. When the chat misroutes a verb, “what did the workspace think was active at the time?” is a clean question with a single answer.
- Defensive update semantics. Workspace-side
setActiveView()can apply diff-based update rules (e.g. ignore no-op updates to silence event spam) once for all consumers.
Layer 2 — Slab-assembly contract
Design
Today services/workspace/src/chat.ts assembles 4 cache-eligible
slabs + 2 volatile slabs. The cache discipline is correct (the
prompt cache wants long stable prefixes; volatile content goes
last). The architecture needs to name every dimension the chat
sees so additions land in the right slab consistently and don’t
silently burst the cache.
The locked slab inventory:
Cache-eligible slabs (in cache order — earlier = bigger cache wins)
STATIC_SPINE— what the chat is. Augment-It’s identity, the three response modes, the strict-alignment discipline. Pinned to the chat package version; changes only on a release commit.CAPABILITY_ROSTER— the verb roster, rendered from the Layer 3 registry. Changes whenever a verb is added or a verb’s args schema changes (rare; one cache miss per release).ACTIVE_SKILLS— opt-in skill packs the operator has enabled for this session. Empty today, reserved breakpoint.PER_CLIENT_DISCIPLINE— per-client rules (e.g. “this client’s funder corpus is strictly funder-domain-only,” “this client uses controlled tag vocabulary X”). Loaded from the per-client repo’sagent-discipline.mdif present; empty otherwise. Cache hit per client.
Volatile slabs (no cache_control; cheap to recompute)
ACTIVE_VIEW_SLAB— the currentActiveViewrendered as prose, e.g. “The user is in Content Reader, scoped to clientreach-eduand record setmaster-pipeline-tracker-v6(783 records), focused on rowgriffin-catalyst.” This is the slab that gives the chat surface-awareness.RECENT_ACTIVITY_SLAB— last-N capability outcomes, e.g. “In the last 5 minutes the user has: added 3 URLs toarthur-blank-foundationcorpus, fired the entity-blog pack against 14 rows, then opened Inbox.” Inlined recent-capabilities ring buffer from Layer 1.AMBIENT_COUNTS_SLAB— “6 items in inbox awaiting triage; 12 responses in Request Reviewer pending review.” A small set of “what’s waiting” counters the chat surface can mention without the user having to ask. Counters are cheap workspace queries.SUGGESTIONS_SLAB— the existingsuggestedVerbsSlab(). Stays at the bottom.
Locked semantics per slab
The point of naming the slabs isn’t bureaucracy — it’s so that the next agent who adds a context dimension knows which slab to put it in. Locked rules:
- Anything user-identity-shaped goes in
STATIC_SPINE(the chat’s character) orPER_CLIENT_DISCIPLINE(the client’s character). Never in volatile slabs. - Anything that changes per view goes in
ACTIVE_VIEW_SLAB. Includingactive_client_ideven though the client is fairly stable — because the slab itself is volatile, and rendering it consistently in one place beats spreading client info across multiple slabs. - Anything that’s “what happened” goes in
RECENT_ACTIVITY_SLAB. - Anything that’s “what’s waiting” goes in
AMBIENT_COUNTS_SLAB. - Capability suggestions come last so they’re nearest the user’s message (a small recency boost for the model’s attention).
Cache miss budget
Adding a slab dimension to the cache-eligible region (1-4) costs one cache miss per change. Volatile slabs (5-8) cost no cache delta — they’re after the cache breakpoint. New context dimensions should default to volatile unless the dimension is genuinely stable per-session.
Layer 3 — Verb registry
Design
V001_CHAT_VERBS becomes a derived string, not a hand-written one.
The source of truth lives in a new file
services/workspace/src/chat-verb-registry.ts:
export type ChatVerbDescriptor = {
capability: string; // e.g. 'corpus.inbox.add'
one_line: string; // model-facing tagline
args_schema: JSONSchema; // for the chat_propose tool's args field
recognition_hints?: string[]; // explicit "/foo <bar>" patterns
prefer_invoke_when?: string; // prose hint to the model
prefer_propose_when?: string;
context_required?: ('active_client_id' | 'active_record_set_id' | 'active_row_id')[];
};
export const CHAT_VERBS: ChatVerbDescriptor[] = [
{
capability: 'corpus.inbox.add',
one_line: 'Save a URL to the operator\'s Corpus Inbox for later triage.',
args_schema: { /* JSON schema */ },
recognition_hints: [
'/inbox <url>',
'/inbox <url> [note]',
'/inbox <url> #tag1 #tag2',
'"save this" / "park this" / "inbox this" + URL',
],
prefer_invoke_when: 'user explicitly types /inbox or names "save / park / inbox / remember" alongside a URL',
prefer_propose_when: 'a URL appears in conversation but operator intent is unclear',
context_required: ['active_client_id'],
},
// ...
];
Rendering:
CAPABILITY_ROSTERslab isCHAT_VERBS.map(renderForModel).join('\n').- The
chat_proposeandchat_invoketool definitions’ enum oncapabilityisCHAT_VERBS.map(v => v.capability). - Each verb’s
context_requiredis checked at slab-assembly time: if the verb needsactive_client_idbut the workspace doesn’t have one, the chat surface emits a warning and the verb is hidden from the roster for that session (prevents the model from proposing verbs it can’t actually run).
How verbs land going forward
To add a new chat verb:
- Ship the backend capability + workspace router entry (unchanged process).
- Add a
ChatVerbDescriptorentry toCHAT_VERBS. The roster prose + the tool’s enum update automatically. - (Optional) Add a
recognition_hintsline for explicit-verb patterns the model should treat aschat_invokecandidates.
No prose-block edits. No tool-schema edits. The cache misses once on the next chat turn after the registry changes (because slab 2 content changes), then it’s stable again.
What this doesn’t do (yet)
- Per-verb gating — some verbs need destructive-action gating
(e.g. a future
corpus.discard); the registry has a hook for it but the gating UX is a separate question. - Argument auto-validation — the model fills in args matching
the JSON schema, but cross-arg constraints (e.g. “
row_limit≤record_set.row_count”) still happen at the capability handler. - Skill-pack scoping —
ACTIVE_SKILLSlets the operator enable/disable groups of verbs (e.g. “I’m only doing corpus work today, hide the prompt verbs”). Hooks reserved; UX TBD.
Migration plan
- Layer 1 — workspace
ActiveViewextension +active_client_id. The largest single change; touches every microfrontend that needs to push state. Could land in a single named branch with one PR per microfrontend, OR (lean) trunk-style with one PR per microfrontend over a week. Either way, the chat keeps working throughout because the slab assembly checks for variant exhaustively viakindswitch. - Layer 2 — slab assembly cleanup. Once Layer 1 is in,
contextSlab()gets renamed and split into the four volatile slabs above. AddPER_CLIENT_DISCIPLINEplumbing (reads the per-client repo’sagent-discipline.mdif present). The chat’s answers immediately get richer; same verb roster. - Layer 3 — verb registry. Extract
CHAT_VERBSinto the registry file. Cache-eligible slab 2 becomes derived. The next new verb (likelycorpus.inbox.list, the inbox triage path, or one of the pack/connector verbs) lands as a registry entry. - (Parallel) as verbs are added, each microfrontend’s
anticipation-
suggest()function reads from the registry too — “what verbs is this view a good launching pad for?”
Per the [[branch-cadence]] feedback memory: Layer 1 is a named branch (multi-microfrontend); Layer 2 is trunk; Layer 3 is a single named branch (refactor with no behaviour change). The architecture is designed so each layer ships independently and the chat keeps working between layers.
What this spec is NOT trying to decide
- Which model. Sonnet 4.6 is the v0.0.1 chat model and stays the default; per-verb model overrides are a v2 question (e.g. the conversational-paste intent classifier in [[Corpus-Inbox-Capture-and-Triage]] Vector 2b uses Haiku-class, not the chat default).
- Cross-client work in one session. Workspace’s
active_client_idis single-valued. If the operator wants to research two clients in parallel that’s a separate UX question (paired-mode shell? per-tab client?) that doesn’t gate this architecture. - Skill-pack scoping UX.
ACTIVE_SKILLSslab is reserved; what enables/disables it is its own spec. - Memory across sessions. Today the chat thread_id is per-tab. Persistent threads, summarization, recall — all v2 questions.
- Multi-turn capability chains. The chat invokes one capability
per turn. Chained workflows (e.g. “draft → improve → apply” as
one user request) live in the verb registry’s
prefer_invoke_whensemantics, not in a separate orchestration layer.
Open questions
- Heartbeat for stale
ActiveView. When a microfrontend unmounts without resetting (browser tab crash, network blip),ActiveViewis stuck. Should workspace expire it after N seconds of no heartbeat from any remote, fall back toidle? Lean: yes, 60s heartbeat with a 10s expiry window. Cheap. PER_CLIENT_DISCIPLINEsource-of-truth. The per-client repo is the natural home for client-specific chat discipline. Format foragent-discipline.md— free-form markdown (paste into slab as-is) or structured (parsed into a typed schema)? Lean: free-form for v1; clients write their own; structure emerges.- Verb registry visibility to other surfaces. The microfrontends
themselves want to know about the verbs (for the connector
palette, for the chat-prompt suggestions in other surfaces).
Should the registry live in
packages/workspace/rather thanservices/workspace/? Lean: yes — types in the package, the per-verb prose stays server-side (so model-facing content can evolve without a remote rebuild). RECENT_ACTIVITY_SLABsummarization. A ring buffer of 10 capability events × medium-length prose lines = ~500 tokens at worst. Cheap, but: should the model see raw event lines or a summarized rollup? Lean: raw lines for v1; summarize when the buffer is full and fewer than 3 distinct verbs are involved (then the rollup line “added 6 URLs to inbox in the last 5 minutes” is more useful than 6 individual events).- Multi-language verb recognition.
recognition_hintsare English-only today. v2 candidate; not blocking. - Verb gating tier. Mutating verbs (
corpus.discard,record_set.delete) wantchat_proposeonly — neverchat_invoke. The registry should carry agating: 'mutate' | 'safe'field that the slab-assembly translates into a discipline rule. v1 leans on author discipline; v2 adds the field formally.
See also
- [[Corpus-Inbox-Capture-and-Triage]] — the first non-prompt chat
verb. The shipped
/inboxverb is the load-bearing demo for this architecture; its hardcodedactive_client_idin the v0.0.1 slab assembly is the cleanup target. - [[../explorations/Agent-Chat-Skills-and-Commands-Candidates]] — the running list of candidate verbs that will populate the Layer 3 registry as they ship. This spec is the architectural counterpart to that exploration.
- [[../blueprints/Chat-As-Verb-Surface-Patterns]] — (in ai-labs/ context-v/) the five patterns this implementation instances: capability adapters, lifecycle events, anticipation, three response modes, four cache-eligible slabs. This spec extends the fifth pattern from “four slabs” to “four cache + four volatile, with a locked inventory.”
- [[Response-Reviewer-Shell-and-Content-Reader-Mode]] — the
microfrontend whose Content Reader mode is the first non-trivial
ActiveViewvariant to design. Its three view-modes (by-response, by-record, content-reader) are the model for how a remote’s internal view-state maps to the workspace-levelActiveView.kind. - [[Packs-and-Bundles-Pattern]] (blueprint) — the verb registry’s
context_requiredsemantics extend cleanly to pack/bundle verbs that need a record-set context. As pack verbs join the registry this connection grounds.