feat(mode): exit_plan_mode + the ACP session-mode picker + scriptable review answers
Plan mode's stage 2 (RFC 2026-07-07-plan-mode). The exit tool: one required plan argument (the durable log artifact), execute re-checks the folded mode, then conducts the review over the user-interaction seam — one single-select question (Approve / Keep planning) with free text open — so an approval appends mode/set back to default in-turn and every other outcome (keep-planning feedback verbatim, aborted, no provider) returns the corrective isError with the mode unchanged. presentCall is a generic card titled by the plan's first heading carrying the plan markdown; over ACP the review rides the ask_user elicitation flow, in the terminal the stdio prompt queue — no approval-seam dependency. The ACP bridge maps the picker 1:1 onto ctx.modes (opportunistic, a type-only peer edge): session/new + session/load advertise availableModes/currentModeId, session/set_mode validates through set() and echoes an optimistic current_mode_update (the pending mode IS the selection; the logged mode/set lands at the boundary and, matching, is not re-sent), and a session/event listener re-notifies on each logged flip that differs from the last sent — the tool-driven exit updates the picker. The feature matrix rows move from 'not modeled' to the picker-to-modes / knobs-to-config-options division, with the ACP v2 removal direction recorded as a mechanical-migration risk. The snapshot harness gains the setMode/setModeExpectError ops and a scripted elicitationAnswers FIFO (cancel on exhaustion; a stray choice string reaches the agent verbatim as a non-consenting custom answer, so a scenario bug fails safe). The suite factory's header-pin requirement now applies only to model-turn scenarios — a protocol-only suite has no header content to anchor. examples/plan-acp-agent is the live composition; its keyless modes-advertise scenario pins the wire surface (advertisement, both set_mode round-trips, unknown-id rejection). The recorded plan-mode approve/reject arc awaits a with-key recording session; its texts are pinned at the unit tier meanwhile. examples/AGENTS.md ceiling 653 → 680: the new example's required smoke row does not fit the old budget.
This commit is contained in:
38
examples/plan-acp-agent/tests/acp.snapshot.ts
Normal file
38
examples/plan-acp-agent/tests/acp.snapshot.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineAcpSnapshotSuite, type Scenario } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
|
||||
/**
|
||||
* Snapshot suite for the plan-mode composition (`../cordis.yml`, swapped to
|
||||
* the sibling `cordis.snapshot.yml` replay overlay by the bin under
|
||||
* `DSH_SNAPSHOT=replay`).
|
||||
*
|
||||
* Deliberately ABSENT (pending a with-key recording session — the plan-mode
|
||||
* RFC's recorded-scenario section): the two model-turn scenarios driving the
|
||||
* full arc (setMode → explore → denied write → exit_plan_mode → a scripted
|
||||
* `elicitationAnswers` approve, and the keep-planning sibling). Their deny and
|
||||
* keep-planning texts are meanwhile pinned at the unit tier
|
||||
* (packages/mode/mode/tests) and the ACP mode round-trip in the bridge's
|
||||
* protocol tests (packages/ui/acp/tests/modes.spec.ts).
|
||||
*/
|
||||
const SCENARIOS: Scenario[] = [
|
||||
// Protocol-only (keyless, authored): the session-mode surface this
|
||||
// composition adds. No model turn runs, so no header pin
|
||||
// is needed here — the pinning scenario arrives with the recorded plan-mode
|
||||
// arc. Composition-wise it adds — availableModes/currentModeId advertised on
|
||||
// session/new, the optimistic current_mode_update a session/set_mode
|
||||
// answers with, and the loud rejection of an unknown mode id — as committed
|
||||
// wire bytes. No model turn, so it replays keyless.
|
||||
{ name: 'modes-advertise', hasModelTurn: false, recorded: false },
|
||||
]
|
||||
|
||||
defineAcpSnapshotSuite({
|
||||
agent: {
|
||||
binScript: fileURLToPath(new URL('../../../packages/ui/acp-agent/src/bin.ts', import.meta.url)),
|
||||
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
||||
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
|
||||
},
|
||||
snapshotsDir: join(dirname(fileURLToPath(import.meta.url)), 'snapshots'),
|
||||
scenarios: SCENARIOS,
|
||||
mode: process.env.DSH_SNAPSHOT === 'record' ? 'record' : 'replay',
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"op": "initialize"
|
||||
},
|
||||
{
|
||||
"op": "newSession"
|
||||
},
|
||||
{
|
||||
"op": "setMode",
|
||||
"modeId": "plan"
|
||||
},
|
||||
{
|
||||
"op": "setMode",
|
||||
"modeId": "default"
|
||||
},
|
||||
{
|
||||
"op": "setModeExpectError",
|
||||
"modeId": "yolo"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
|
||||
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"}}}
|
||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"plan"}}}
|
||||
{"jsonrpc":"2.0","id":3,"result":{}}
|
||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"default"}}}
|
||||
{"jsonrpc":"2.0","id":4,"result":{}}
|
||||
{"jsonrpc":"2.0","id":5,"error":{"code":-32602,"message":"Invalid params: unknown mode \"yolo\" — available modes: default, plan"}}
|
||||
Reference in New Issue
Block a user