docs/lean-context-architecture.md

Lean-Context Architecture (Two-Tier Progressive Disclosure)

Source Specs: lean-context-prompt.md, v2-cache-harness-prd.md, the engine plugin ecosystem notes

Picasso builds agent context using two-tier progressive disclosure so every turn sends only what that turn needs while preserving a 100% byte-stable prompt-cache prefix across turns, permission modes (default / plan / auto), and surfaces (apps/cli + packages/tui and apps/mac + apps/desktop-helper).

Nothing is disabled or deleted: every tool, skill, memory, and MCP server remains enabled and discoverable, costing 0 payload tokens until requested on demand.

---

1. Tier 1 vs. Tier 2 Split

Tier 1 — Always-On Routing Index (Byte-Stable Prefix)

Tier 1 sits in the static request prefix (tools -> system -> initial instruction index) and must be byte-identical across turns and modes within a session:

Tier 2 — On-Demand Payloads (Appended History)

Tier 2 payloads cost 0 prefix tokens until triggered and always enter as appended conversation history (tool results or sourced agent/pre-step user-role reminders):

---

2. Prompt-Cache Stability Invariants

  1. Never mutate `tools` mid-session: Adding or removing tool definitions invalidates the entire provider KV cache (tools -> system -> messages). All deferred and MCP tools route through fixed-schema gateway tools.
  2. Identical Tier 1 across modes: Switching between default, plan, and auto modes keeps the exact same tool catalog and system prefix; mode transitions append state notices via agent/pre-step.
  3. Cross-surface parity: apps/cli (packages/core/src/dsh/runtime-session.ts) loads dsh/picasso-ecosystem.cordis.patch.yml and dsh/picasso-sdk.patch.yml; apps/desktop-helper (its engine host module) does not: it composes the engine's base and web-app bundles plus generated rows. Both surfaces share the packages/picasso-* plugins. Tier 1 byte-equality across surfaces therefore depends on the two compositions exposing the same core tools and system layers, and is a property to verify (with picasso-cache-probe and the Tier-1 hash tests), not one the shared patch files guarantee. The generated dsh/tool-catalog is derived from the CLI patch only, so it can advertise deferred tools the Mac does not mount until the matching Mac plugin rows land.
  4. Verification: Use packages/picasso-cache-probe and packages/cache-harness (see docs/cache-harness.md) to assert zero cached-prefix drops on turn 2+ after an on-demand Tier 2 load and ≥ 85% reduction in hello first-turn prefix bytes against reports/lean-context/baseline.md (the unit-level no-MCP floor in packages/picasso-lean-context is 83%: the ~2.1 KB style/practice sections were bought with budget, not by cutting tool rules).

---

3. /context Inspector (Observational Telemetry)

packages/picasso-lean-context exports a ContextStats collector and a Config schema that publish four `.volatile()` fields under the picasso-lean-context settings namespace. settings/describe projects them as the namespace value — the read path the Mac app's Context inspector sheet uses (RelayClient+Context.swift → remote("settings/describe") → ns == "picasso-lean-context").

FieldContent
registryTier-1 wire tool count, deferred tool count, MCP gateways + server names, known skills — refreshed at each system-prompt/assemble and agent/pre-step.
snapshotThe last pre-step normalisation: message count, instruction and skill-catalog before/after bytes + cap flags, SHA-1/pointer dedupe drops, injected plan-mode/runtime-policy/stale-todos reminders, skillsKnown, sessionId.
countersCumulative totalSteps, instructionsCapped, skillsOverflowed, dedupeRuns, remindersInjected.
eventsCompression journal, newest first, ring-capped at CONTEXT_STATS_EVENT_LIMIT (50).

Observational-only invariant: every capture point runs *after* the normalisers have already selected their outputs (ContextStats.recordStep reads normalizePreStepMessages's additive stats return; captureRegistry reads the post-sort wireMap/deferredTools/mcpServers sizes). Nothing feeds stats back into decision.messages, assembly.tools, or sections — so a decision's emitted bytes, tool schemas, and ordering are untouched whether stats are collected or not. Without the engine's schema library, Config is undefined and capture still works for deps.stats consumers; the Mac sheet renders a named .unavailable state when the namespace is absent.

Entry points: View › Context inspector, ⌃⌥C, or the command palette's "Context inspector" action (ButtonActionID.openContextInspector → NativeSheet.contextInspector).

---

4. Context timeline (/context timeline, picasso context timeline)

The @picasso/lean-context/timeline fold turns a session log into a per-turn timeline: what entered the context and its size, each model step's cache hit and miss, what compaction removed or summarised, and one lane per subagent. The SDK server serves it as session/context/timeline (packages/picasso-sdk-server/context-timeline.js) for the TUI's /context timeline; picasso context timeline [thread] [--last] [--json] folds the same thing from a saved session's log with no engine running and no model call (apps/cli/src/context-timeline.ts). Picasso for Mac shows it under the Context tab's Overview / Timeline switch. A field the engine did not record prints as not recorded (the Mac app says "No usage reported" or "Size not recorded"), never as a guess (C1).