diff --git a/docs/rfc/proposed/feature/2026-07-07-plan-mode.md b/docs/rfc/proposed/feature/2026-07-07-plan-mode.md index e1a0d65ac8..0a2f9890f6 100644 --- a/docs/rfc/proposed/feature/2026-07-07-plan-mode.md +++ b/docs/rfc/proposed/feature/2026-07-07-plan-mode.md @@ -58,7 +58,7 @@ The mode PICKER is this package's surface: `session/new`/`session/load` advertis ### For agent creators -`ctx.modes` is the whole programmatic surface: `list()` returns the configured definitions plus the synthetic `default` entry (for pickers), `get(agent)` returns the folded mode plus any pending intent, and `set(agent, mode)` validates the name against config and records the boundary-applied intent. A creator seeds a child's initial mode through `AgentOptions.mode` (`AgentOptions` is merge-extensible; `dsh-mode` declares the optional field). There is no live `agent/*` mirror to subscribe: UIs read `mode/set` off `session/event`, per [event-domain semantics](../../implemented/architecture/2026-06-30-event-domain-semantics.md). +`ctx.modes` is the whole programmatic surface: `list()` returns the configured definitions plus the synthetic `default` entry (for pickers), `get(agent)` returns the folded mode plus any pending intent, and `set(agent, mode)` validates the name against `list()`'s vocabulary and records the boundary-applied intent — `default` is always a valid target, so exiting a mode is the same call as entering one. A creator seeds a child's initial mode through `AgentOptions.mode` (`AgentOptions` is merge-extensible; `dsh-mode` declares the optional field). There is no live `agent/*` mirror to subscribe: UIs read `mode/set` off `session/event`, per [event-domain semantics](../../implemented/architecture/2026-06-30-event-domain-semantics.md). ## Detailed design @@ -86,7 +86,7 @@ The allowlist is deliberately the degenerate form of a future per-tool decision ### The fold, the service, and the flush -`foldMode(events)` is pure (exported for reconstructors and tests); the service tracks it per session with a lazy cursor in a `WeakMap` — O(new events) per read, never invalidated, because the log is append-only and `mode/set` is not a surface node (compaction cannot rewrite it). `set(agent, mode)` validates the name against config, drops a no-op (target equals pending ?? current), and otherwise records the intent in a `WeakMap` — it cannot append immediately, because [every session event is turn-enclosed](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md) and an idle agent has no open turn. +`foldMode(events)` is pure (exported for reconstructors and tests); the service tracks it per session with a lazy cursor in a `WeakMap` — O(new events) per read, never invalidated, because the log is append-only and `mode/set` is not a surface node (compaction cannot rewrite it). `set(agent, mode)` validates the name against `list()`'s vocabulary — the configured definitions plus the reserved `default`, which is rejected as a config KEY but always accepted as a `set()` TARGET (a picker's exit-to-default must be a valid write) — drops a no-op (target equals pending ?? current), and otherwise records the intent in a `WeakMap` — it cannot append immediately, because [every session event is turn-enclosed](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md) and an idle agent has no open turn. A contained `session/event` listener ([defensive patterns](../../../defensive-patterns.md): a policy plugin must not kill the feed) flushes the pending intent as a `mode/set` append on the next `turn/start` or `step/end` — both sit outside the step's tool-execution window, so the executions of a step always run under the mode its assembly folded — and, when the flushed mode differs from the fold at the last `request/header`, appends one coalesced `context/message` notice in the same frame ("The user switched this session to plan mode."); the user-visible narration cases are enumerated in the [FAQ](#faq). Seeding rides `agent/created`: `AgentOptions.mode` becomes a pending intent, so explicit options beat the logged baseline on create AND resume — the same precedence the call-config seed follows.