| ... | @@ -1,564 +0,0 @@ |
| 1 | # Plan Mode Deep Dive |
| 2 | |
| 3 | ## Goal |
| 4 | |
| 5 | Implement plan mode without fighting the architecture that already exists in this repo. |
| 6 | |
| 7 | The current app is: |
| 8 | |
| 9 | - provider-adapter driven on the server |
| 10 | - orchestration snapshot driven on the client |
| 11 | - already equipped with some plan-adjacent runtime events |
| 12 | - not yet equipped with first-class plan-mode state in the orchestration read model |
| 13 | |
| 14 | This document updates the implementation plan to match that reality. |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## Current Repo Baseline |
| 19 | |
| 20 | ### 1. Shared contracts already have some plan primitives |
| 21 | |
| 22 | `packages/contracts/src/providerRuntime.ts` already includes: |
| 23 | |
| 24 | - `turn.plan.updated` |
| 25 | - `content.delta` with `streamKind: "plan_text"` |
| 26 | - `user-input.requested` |
| 27 | - `user-input.resolved` |
| 28 | |
| 29 | Important constraint: |
| 30 | |
| 31 | - these are runtime-event primitives only |
| 32 | - there is no first-class provider capability schema |
| 33 | - there is no first-class plan operating mode field |
| 34 | - there is no dedicated final-plan artifact schema |
| 35 | |
| 36 | ### 2. Existing `runtimeMode` is not plan mode |
| 37 | |
| 38 | `RuntimeMode` in `packages/contracts/src/orchestration.ts` and `packages/contracts/src/provider.ts` means execution policy: |
| 39 | |
| 40 | - `approval-required` |
| 41 | - `full-access` |
| 42 | |
| 43 | Plan mode must not overload that field. |
| 44 | |
| 45 | If we need provider operating mode, it needs a separate concept and separate naming, for example: |
| 46 | |
| 47 | - `interactionMode` |
| 48 | - `planningMode` |
| 49 | - `providerOperatingMode` |
| 50 | |
| 51 | Anything is acceptable as long as it is not reused from the existing access-policy `runtimeMode`. |
| 52 | |
| 53 | ### 3. Server flow is already canonical-event-first |
| 54 | |
| 55 | The current server pipeline is: |
| 56 | |
| 57 | 1. Provider adapters emit `ProviderRuntimeEvent`s. |
| 58 | 2. `ProviderService` multiplexes adapter streams and maintains provider session bindings. |
| 59 | 3. `ProviderRuntimeIngestion` translates runtime events into orchestration commands and thread activities. |
| 60 | 4. `ProjectionPipeline` persists read-model tables. |
| 61 | 5. `ProjectionSnapshotQuery` rebuilds the orchestration snapshot returned by `orchestration.getSnapshot`. |
| 62 | 6. `wsServer` exposes generic orchestration snapshot, command dispatch, diff, and replay methods. |
| 63 | |
| 64 | This matters because plan mode should fit into the same pipeline, not bypass it with a parallel websocket protocol. |
| 65 | |
| 66 | Important detail: |
| 67 | |
| 68 | - `wsServer` broadcasts orchestration domain events, not raw provider runtime events |
| 69 | - if the web should see plan state live, that state must first become orchestration events and/or projections |
| 70 | |
| 71 | ### 4. The current read model has nowhere to put plan mode |
| 72 | |
| 73 | `OrchestrationThread` currently contains: |
| 74 | |
| 75 | - `messages` |
| 76 | - `activities` |
| 77 | - `checkpoints` |
| 78 | - `session` |
| 79 | - `latestTurn` |
| 80 | - thread metadata like title, model, branch, worktree |
| 81 | |
| 82 | It does not contain: |
| 83 | |
| 84 | - current structured plan snapshot |
| 85 | - accumulated plan-text stream |
| 86 | - pending structured user-input prompt |
| 87 | - resolved structured answers |
| 88 | - provider interactive capabilities |
| 89 | - provider plan/default operating mode |
| 90 | - final approved plan artifact |
| 91 | |
| 92 | Today the only flexible place plan data can survive into the web snapshot is `activities[].payload`. |
| 93 | |
| 94 | ### 5. The web app is snapshot-driven, not event-reduced |
| 95 | |
| 96 | The client flow today is: |
| 97 | |
| 98 | 1. `EventRouter` listens to `orchestration.domainEvent`. |
| 99 | 2. On domain events, it re-fetches `orchestration.getSnapshot`. |
| 100 | 3. Zustand stores the latest normalized snapshot. |
| 101 | 4. `session-logic.ts` derives UI state like pending approvals and work log from thread activities. |
| 102 | 5. `ChatView` renders the thread from snapshot state plus those derived selectors. |
| 103 | |
| 104 | Important implication: |
| 105 | |
| 106 | - plan mode should be added to the orchestration snapshot and/or activity derivation path |
| 107 | - it should not depend on a separate client-side event reducer |
| 108 | |
| 109 | ### 6. Current implementation status by provider |
| 110 | |
| 111 | #### Codex |
| 112 | |
| 113 | Currently implemented: |
| 114 | |
| 115 | - `turn/plan/updated` maps to `turn.plan.updated` |
| 116 | - `item/plan/delta` maps to `content.delta` with `streamKind: "plan_text"` |
| 117 | |
| 118 | Current gap: |
| 119 | |
| 120 | - `item/tool/requestUserInput` is currently classified as `request.opened` with request type `tool_user_input` |
| 121 | - it is not yet upgraded into `user-input.requested` |
| 122 | - there is no structured answer submission path yet |
| 123 | |
| 124 | #### Claude Code |
| 125 | |
| 126 | Currently implemented: |
| 127 | |
| 128 | - session/config metadata surfaces through `session.configured` |
| 129 | - assistant/reasoning text flows through normal runtime content events |
| 130 | - approval requests already bridge into `request.opened` / `request.resolved` |
| 131 | |
| 132 | Current gap: |
| 133 | |
| 134 | - no current canonical plan-mode mapping |
| 135 | - no current `AskUserQuestion` mapping |
| 136 | - no current `ExitPlanMode` mapping |
| 137 | |
| 138 | #### Cursor |
| 139 | |
| 140 | Currently implemented: |
| 141 | |
| 142 | - `available_commands_update` maps to `session.configured` |
| 143 | - thought/message chunks map to `content.delta` |
| 144 | - permission requests bridge into approval request events |
| 145 | |
| 146 | Current gap: |
| 147 | |
| 148 | - no current plan-mode mapping |
| 149 | - no current structured prompt mapping |
| 150 | - no current todo/plan projection |
| 151 | |
| 152 | --- |
| 153 | |
| 154 | ## Design Constraints |
| 155 | |
| 156 | ### Keep the current transport model |
| 157 | |
| 158 | Plan mode should continue to flow through: |
| 159 | |
| 160 | - `ProviderRuntimeEvent` |
| 161 | - orchestration commands/events |
| 162 | - projection tables |
| 163 | - `orchestration.getSnapshot` |
| 164 | - `orchestration.domainEvent` |
| 165 | |
| 166 | Do not introduce provider-specific websocket channels unless the current architecture proves insufficient. |
| 167 | |
| 168 | ### Keep the client snapshot-driven |
| 169 | |
| 170 | The client already assumes that server state is re-synced from snapshots. Plan mode should respect that. |
| 171 | |
| 172 | That means: |
| 173 | |
| 174 | - server-side persistence matters more than clever client buffering |
| 175 | - reconnect behavior should come from projection state, not ad hoc React state |
| 176 | - `session-logic.ts` and `ChatView` are the main frontend extension seams |
| 177 | |
| 178 | ### Keep approvals separate from structured user input |
| 179 | |
| 180 | The current system has a full approval pipeline: |
| 181 | |
| 182 | - runtime `request.opened` / `request.resolved` |
| 183 | - orchestration `thread.approval.respond` |
| 184 | - provider `respondToRequest(...)` |
| 185 | |
| 186 | Plan-mode structured prompts are different and should not be forced into the approval model unless a provider genuinely models them as approval requests. |
| 187 | |
| 188 | ### Preserve raw provider payloads |
| 189 | |
| 190 | When adding plan-mode mappings: |
| 191 | |
| 192 | - keep `raw` on internal canonical runtime events |
| 193 | - keep `providerRefs` on internal canonical runtime events |
| 194 | - avoid throwing away native payload shape too early inside adapter/server processing |
| 195 | |
| 196 | This does not mean provider-specific payloads should leak into the orchestration read model, websocket API, or web UI. |
| 197 | |
| 198 | The boundary should stay: |
| 199 | |
| 200 | - adapter/server internals may retain native payloads for correlation, debugging, and future parser improvements |
| 201 | - shared orchestration state and client-visible contracts should expose only canonical plan-mode data |
| 202 | |
| 203 | That matters most for Claude and Cursor, where some mappings will be adapter-derived and may need native context during implementation and debugging. |
| 204 | |
| 205 | --- |
| 206 | |
| 207 | ## Updated Shared Work Plan |
| 208 | |
| 209 | ## 1. Tighten shared contract naming before wiring features |
| 210 | |
| 211 | The first step is not to invent a new architecture. It is to formalize the concepts that are currently missing from contracts. |
| 212 | |
| 213 | ### Add a plan operating mode concept |
| 214 | |
| 215 | Add a new shared contract for provider plan/default mode. |
| 216 | |
| 217 | Requirements: |
| 218 | |
| 219 | - do not reuse `RuntimeMode` |
| 220 | - keep it orthogonal to access policy |
| 221 | - make it available in the server->web read model, not only in raw runtime payloads |
| 222 | |
| 223 | Preferred shape: |
| 224 | |
| 225 | ```ts |
| 226 | type ProviderInteractionMode = "default" | "plan"; |
| 227 | ``` |
| 228 | |
| 229 | ### Add typed provider interactive capabilities |
| 230 | |
| 231 | Current `session.configured.payload.config` is just `Record<string, unknown>`. That is too weak for capability-driven UI. |
| 232 | |
| 233 | Add a typed capability surface in `packages/contracts` that both server and web can import directly. |
| 234 | |
| 235 | Preferred shape: |
| 236 | |
| 237 | - exported constant/object keyed by `ProviderKind` |
| 238 | - exported type derived from that object |
| 239 | - usable by adapters, orchestration code, and frontend rendering logic without duplicating provider capability tables |
| 240 | |
| 241 | Minimum fields: |
| 242 | |
| 243 | ```ts |
| 244 | interface ProviderInteractiveCapabilities { |
| 245 | supportsPlanMode: boolean; |
| 246 | supportsStructuredPlanUpdates: boolean; |
| 247 | supportsPlanTextStreaming: boolean; |
| 248 | supportsStructuredUserInput: boolean; |
| 249 | supportsFreeformUserInput: boolean; |
| 250 | supportsPlanAcceptance: boolean; |
| 251 | } |
| 252 | ``` |
| 253 | |
| 254 | Preferred usage: |
| 255 | |
| 256 | - static provider capabilities live in shared contracts |
| 257 | - adapters may still emit dynamic runtime metadata when a capability is session-specific or probe-dependent |
| 258 | - the orchestration snapshot should expose the active provider and any dynamic overrides, not duplicate the full static capability catalog per thread |
| 259 | |
| 260 | ### Add a dedicated structured-answer command path |
| 261 | |
| 262 | Current shared commands only support approvals: |
| 263 | |
| 264 | - `thread.approval.respond` |
| 265 | - provider `respondToRequest(...)` |
| 266 | |
| 267 | Plan mode needs a distinct path for structured prompt answers. |
| 268 | |
| 269 | Preferred additions: |
| 270 | |
| 271 | - orchestration command such as `thread.user-input.respond` |
| 272 | - provider service method such as `respondToUserInput(...)` |
| 273 | - adapter hook per provider for native answer serialization |
| 274 | |
| 275 | Do not overload approval decisions like `accept` / `decline` for structured question answers. |
| 276 | |
| 277 | ## 2. Extend the orchestration read model |
| 278 | |
| 279 | The current read model is the main missing piece. |
| 280 | |
| 281 | Plan mode needs first-class projection state so reconnect and refresh behave like the rest of the app. |
| 282 | |
| 283 | ### Add first-class thread plan state |
| 284 | |
| 285 | Preferred thread-level additions: |
| 286 | |
| 287 | - `interactionMode` |
| 288 | - `currentPlan` |
| 289 | - `pendingUserInput` |
| 290 | - `resolvedUserInputs` |
| 291 | - optional `finalPlanArtifact` |
| 292 | |
| 293 | The exact nesting can vary, but it should be snapshot-friendly and not require replaying arbitrary activity payloads in the client. |
| 294 | |
| 295 | `interactiveCapabilities` should not be modeled as thread-owned state if they are static per provider. |
| 296 | |
| 297 | Preferred split: |
| 298 | |
| 299 | - static capabilities: shared contract export keyed by provider |
| 300 | - dynamic per-thread/per-session state: only fields that can actually vary at runtime, such as current interaction mode or active pending prompt |
| 301 | |
| 302 | ### Keep activities as secondary audit trail |
| 303 | |
| 304 | `thread.activities` should still receive plan/user-input activity summaries for timeline/debugging, but it should not be the only source of truth for active plan state. |
| 305 | |
| 306 | ### Projection changes |
| 307 | |
| 308 | This likely means: |
| 309 | |
| 310 | - new projection repository/table(s) for plan state and prompt state |
| 311 | - `ProjectionPipeline` updates |
| 312 | - `ProjectionSnapshotQuery` returning the new fields |
| 313 | - projection tests for persistence and replay |
| 314 | |
| 315 | ## 3. Update runtime ingestion to populate that state |
| 316 | |
| 317 | `ProviderRuntimeIngestion` already knows how to: |
| 318 | |
| 319 | - turn assistant text into messages |
| 320 | - turn approval events into activities |
| 321 | - turn `turn.plan.updated` into an activity |
| 322 | |
| 323 | It should be extended to also translate plan/user-input runtime events into read-model state. |
| 324 | |
| 325 | Implementation direction: |
| 326 | |
| 327 | - `turn.plan.updated` updates projected plan snapshot |
| 328 | - `content.delta(plan_text)` appends to projected plan-text buffer when relevant |
| 329 | - `user-input.requested` opens a projected pending prompt |
| 330 | - `user-input.resolved` closes/resolves that prompt |
| 331 | - providers that only expose raw/native blobs can still emit synthesized canonical runtime events first, then rely on the shared ingestion path |
| 332 | |
| 333 | ## 4. Integrate plan mode into the existing web seams |
| 334 | |
| 335 | The web architecture already has the right places. |
| 336 | |
| 337 | ### Store and types |
| 338 | |
| 339 | Update: |
| 340 | |
| 341 | - `apps/web/src/types.ts` |
| 342 | - `apps/web/src/store.ts` |
| 343 | |
| 344 | The store should stay thin and snapshot-oriented. It should sync whatever new read-model fields the server exposes. |
| 345 | |
| 346 | ### Derived UI state |
| 347 | |
| 348 | Update: |
| 349 | |
| 350 | - `apps/web/src/session-logic.ts` |
| 351 | |
| 352 | This is the natural place for: |
| 353 | |
| 354 | - `derivePlanState(...)` |
| 355 | - `derivePendingUserInput(...)` |
| 356 | - any fallback derivation from activities during an incremental migration |
| 357 | |
| 358 | ### Rendering |
| 359 | |
| 360 | Primary UI seam: |
| 361 | |
| 362 | - `apps/web/src/components/ChatView.tsx` |
| 363 | |
| 364 | Current UI already has: |
| 365 | |
| 366 | - top-of-thread alert stacks for pending approvals |
| 367 | - work-log rendering derived from activities |
| 368 | - timeline rows for messages and work state |
| 369 | |
| 370 | That suggests two viable rendering patterns: |
| 371 | |
| 372 | 1. active plan/prompt panel above the timeline |
| 373 | 2. resolved plan/prompt entries rendered in timeline/history |
| 374 | |
| 375 | The route structure does not need to change. Plan mode should remain thread-local state inside the existing chat route. |
| 376 | |
| 377 | --- |
| 378 | |
| 379 | ## Provider Implementation Plan |
| 380 | |
| 381 | ## Provider 1: Codex |
| 382 | |
| 383 | Codex is closest to the target shape and should be the first end-to-end implementation. |
| 384 | |
| 385 | ### Current status |
| 386 | |
| 387 | Already present: |
| 388 | |
| 389 | - native structured plan update mapping |
| 390 | - native plan-text stream mapping |
| 391 | |
| 392 | Missing: |
| 393 | |
| 394 | - native structured prompt mapping for `item/tool/requestUserInput` |
| 395 | - structured answer submission path |
| 396 | - removal of the current empty-answer auto-ack path in `codexAppServerManager` |
| 397 | - projection and UI support for the already-emitted plan events |
| 398 | |
| 399 | ### Required work |
| 400 | |
| 401 | 1. Upgrade `item/tool/requestUserInput` from request-only handling to canonical `user-input.requested`. |
| 402 | 2. Preserve `request.opened` as transport/debug metadata when useful. |
| 403 | 3. Add adapter-side answer submission for the corresponding pending request/tool context. |
| 404 | 4. Emit `user-input.resolved` when the answer is submitted or confirmed resolved. |
| 405 | 5. Project the result into the orchestration snapshot. |
| 406 | |
| 407 | ### Codex-specific note |
| 408 | |
| 409 | The runtime event schemas already match Codex reasonably well, so this provider should define the canonical end-to-end behavior first. |
| 410 | |
| 411 | ## Provider 2: Claude Code |
| 412 | |
| 413 | Claude likely needs tool-aware adaptation rather than pure transport mapping. |
| 414 | |
| 415 | ### Current status |
| 416 | |
| 417 | Already present: |
| 418 | |
| 419 | - generic session/runtime stream integration |
| 420 | - approval bridging |
| 421 | |
| 422 | Missing: |
| 423 | |
| 424 | - plan/default operating mode mapping |
| 425 | - `AskUserQuestion` -> `user-input.requested` |
| 426 | - `ExitPlanMode` -> final-plan or plan-state handoff |
| 427 | |
| 428 | ### Required work |
| 429 | |
| 430 | 1. Detect plan/default operating mode from Claude-native metadata without touching access-policy `runtimeMode`. |
| 431 | 2. Map `AskUserQuestion` tool payloads into canonical `user-input.requested`. |
| 432 | 3. Add provider-specific answer serialization for the return path. |
| 433 | 4. Detect `ExitPlanMode` and decide whether it becomes: |
| 434 | - a final-plan artifact |
| 435 | - a synthetic structured plan snapshot |
| 436 | - or both |
| 437 | 5. Avoid over-synthesizing incremental structured plan steps unless Claude gives us enough structure to do it safely. |
| 438 | |
| 439 | ## Provider 3: Cursor |
| 440 | |
| 441 | Cursor should start from the same canonical server pipeline, but with a more conservative adapter. |
| 442 | |
| 443 | ### Current status |
| 444 | |
| 445 | Already present: |
| 446 | |
| 447 | - session/update stream handling |
| 448 | - reasoning/assistant text mapping |
| 449 | - approval bridging |
| 450 | |
| 451 | Missing: |
| 452 | |
| 453 | - plan/default operating mode mapping |
| 454 | - structured prompt mapping |
| 455 | - structured plan/todo mapping |
| 456 | |
| 457 | ### Required work |
| 458 | |
| 459 | 1. Detect and publish Cursor operating mode separately from access-policy runtime mode. |
| 460 | 2. Map documented extension methods such as `cursor/ask_question` and `cursor/update_todos` only when payloads are confirmed. |
| 461 | 3. Fall back to plan-mode indicator plus normal assistant/reasoning text if structured Cursor plan events are absent. |
| 462 | 4. Preserve raw ACP payloads so the adapter can improve without changing higher layers. |
| 463 | |
| 464 | --- |
| 465 | |
| 466 | ## Frontend Rendering Plan |
| 467 | |
| 468 | The frontend should remain capability-driven, but using real repo seams. |
| 469 | |
| 470 | ### Active state |
| 471 | |
| 472 | Render from projected snapshot fields: |
| 473 | |
| 474 | - current plan/default operating mode |
| 475 | - provider capabilities |
| 476 | - current structured plan |
| 477 | - current plan-text stream |
| 478 | - pending structured prompt |
| 479 | |
| 480 | ### History state |
| 481 | |
| 482 | Render from: |
| 483 | |
| 484 | - normal messages |
| 485 | - resolved prompt answers |
| 486 | - plan-related activity/history rows |
| 487 | - optional final-plan artifact |
| 488 | |
| 489 | ### Placement |
| 490 | |
| 491 | Recommended initial placement: |
| 492 | |
| 493 | 1. Active pending prompt and active plan panel above the timeline, near the current approvals stack. |
| 494 | 2. Resolved prompt/plan history in the timeline. |
| 495 | |
| 496 | This fits the existing `ChatView` layout with the least architectural churn. |
| 497 | |
| 498 | --- |
| 499 | |
| 500 | ## Test Plan |
| 501 | |
| 502 | ### Contracts |
| 503 | |
| 504 | Add coverage for: |
| 505 | |
| 506 | - new interaction-mode schema |
| 507 | - new capability schema |
| 508 | - new structured-answer command schema |
| 509 | - any read-model additions for active plan/prompt state |
| 510 | |
| 511 | Retain existing coverage for: |
| 512 | |
| 513 | - `turn.plan.updated` |
| 514 | - `content.delta(plan_text)` |
| 515 | - `user-input.requested` |
| 516 | - `user-input.resolved` |
| 517 | |
| 518 | ### Server |
| 519 | |
| 520 | Add or extend tests for: |
| 521 | |
| 522 | - provider adapter plan/prompt mapping |
| 523 | - `ProviderRuntimeIngestion` plan/prompt projection behavior |
| 524 | - `ProjectionPipeline` persistence and replay |
| 525 | - `ProjectionSnapshotQuery` returning active plan state |
| 526 | - provider answer submission path |
| 527 | |
| 528 | ### Web |
| 529 | |
| 530 | Add or extend tests for: |
| 531 | |
| 532 | - `syncServerReadModel` with new plan fields |
| 533 | - `session-logic.ts` derivation of active plan/prompt state |
| 534 | - `ChatView` rendering of: |
| 535 | - active plan panel |
| 536 | - pending structured prompt |
| 537 | - resolved prompt history |
| 538 | - plan-text fallback |
| 539 | |
| 540 | --- |
| 541 | |
| 542 | ## Recommended Execution Order |
| 543 | |
| 544 | 1. Add shared contract types for interaction mode, capabilities, and structured-answer submission. |
| 545 | 2. Extend orchestration read-model contracts and projection storage for active plan/prompt state. |
| 546 | 3. Wire `ProviderRuntimeIngestion` into those projections. |
| 547 | 4. Finish Codex end-to-end first, since it already emits most of the canonical runtime pieces. |
| 548 | 5. Add frontend snapshot sync, derivation, and rendering in `types.ts`, `store.ts`, `session-logic.ts`, and `ChatView.tsx`. |
| 549 | 6. Add Claude adapter mapping. |
| 550 | 7. Add Cursor adapter mapping and fallback behavior. |
| 551 | |
| 552 | ## Short version |
| 553 | |
| 554 | The repo is already close in one specific sense: the runtime event vocabulary for plans and structured prompts exists. |
| 555 | |
| 556 | The real missing architecture is: |
| 557 | |
| 558 | - a separate plan/default operating mode concept |
| 559 | - typed provider capabilities |
| 560 | - a first-class structured-answer command path |
| 561 | - read-model projection for active plan/prompt state |
| 562 | - web rendering sourced from that snapshot state |
| 563 | |
| 564 | That is the shape the implementation should follow. |