Files
deepseek-harness/.agents/notes/implemented/architecture/2026-07-24-web-plan-mode-projection.md
T

6.1 KiB

Agent Note: Project plan mode through the web host boundary

Status: implemented

English | 中文

Problem

The plan service owned durable state and boundary timing, but the web host contract had no way to discover or select it. A browser could inspect the tail history page for plan/mode, yet that page may omit the latest relevant event after pagination or hold no event for the empty-log inactive state. It also cannot reveal a selection waiting for the next model-request boundary. A client-only toggle would therefore drift from resumed sessions, exit-tool transitions, and selections made by another surface.

The host does not mount plan mode for every product composition. The wire must distinguish an unavailable capability from a supported session whose committed state is inactive. Switching mode is also independent of cancelling an in-progress request: the existing service intentionally applies the latest selection at the next boundary.

Decision

The session RPC domain exposes session.planMode({ sessionId }) and session.setPlanMode({ sessionId, active }). Their shared value is null | { active: boolean, pending?: boolean }. null means the optional ctx.planMode service is absent; { active: false } means the service is available and inactive. Both methods resume a cold session through the same host-owned path as history and prompt before reading or changing state.

The host adapter delegates selection and folding to ctx.planMode; it does not append events or duplicate boundary logic. active is the last committed logged value. When present, pending is the selected target value awaiting a model-request boundary and differs from active; its presence, rather than its boolean value, identifies a user-visible pending transition. Re-selecting the committed value can leave an internal cleanup intent in the service, but the adapter canonicalizes that net-zero state to { active }. The boundary then removes the intent without logging a redundant state event. The wire schema rejects equal active and pending values. The RPC does not cancel a running request, so a selection made during generation leaves that request unchanged and shapes the next one.

The browser session object queries the complete state after history opens and on reconnect. A failed plan query is fail-soft: history remains usable and the last known capability state is retained. A reconnect generation fence prevents a superseded open from overwriting the newer result. One monotonic plan-request fence covers both queries and selections, so an older unary response cannot replace the result of a newer request. A separate local event-version fence prevents a current query or selection response from overwriting a plan/mode commit that overtook it on the mux stream; an early commit remains private until a successful query confirms capability presence. Successful selections otherwise update the snapshot only from the host-confirmed response, while business and transport failures leave the prior state intact.

Committed plan/mode session events remain the live notification. When the host advertised the capability, a valid event replaces active and clears pending. Both the append path and a history replacement window observe the newest valid plan event by sequence, so gap repair applies a recovered commit even when the buffered triggering frame becomes replay overlap. The object layer ignores malformed events and does not infer capability from a raw event alone. This keeps full-state reads authoritative while preserving the existing logged event stream as the commit signal.

State and timing

Starting state Selection Immediate RPC state Next request boundary
Inactive Plan { active: false, pending: true } Logs plan/mode: true; snapshot becomes active
Active Default { active: true, pending: false } Logs plan/mode: false; snapshot becomes inactive
Inactive with pending Plan Default { active: false } No state event is needed
Capability absent Either null No plan behavior is introduced

Stopping generation remains a separate session operation. A pending selection survives cancellation and applies when the next prompt or continuation reaches the service boundary.

Alternatives considered

Fold only the currently loaded history page. Rejected because message-boundary pagination can omit the latest mode event, and a page cannot represent pending intent or distinguish empty-log inactive from capability absence.

Keep an optimistic browser boolean. Rejected because tool-approved exit, another client, resume, and append failure can all disagree with the speculative value. The browser displays only the state returned by the owner.

Add a dedicated plan control frame. Rejected because committed state already has the logged plan/mode event. A full-state unary query covers open and reconnect without adding a second live event vocabulary.

Expose generic named collaboration modes. Rejected by the plan-specific state decision: ACP may keep a generic adapter vocabulary, but the product currently owns one concrete boolean domain.

Verification

  • API schemas reject invalid request and state shapes, including equal committed and pending values, and both fetch directions dispatch the two methods.
  • Host runtime tests cover capability absence, real-service pending state, canonical net-zero cancellation, cold-session errors, and shared RPC semantics.
  • Client object tests cover open, selection success, overlapping selection response order, business and transport failure, committed live events, gap-repaired commits, malformed and unavailable events, fail-soft queries, reconnect refresh, superseded-query fencing, and mux commits overtaking unary responses.

Consequences

Web UI packages can discover plan mode without importing its host implementation and can display boundary-pending state without duplicating the plan service. Other clients may use the same optional projection. The contract deliberately does not combine switching with stop, invent generic mode identifiers, or make plan capability mandatory for every host composition.