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:
kingwl
2026-07-10 02:57:40 +08:00
parent 63ced3e0e2
commit edc065666f
43 changed files with 947 additions and 50 deletions

View File

@@ -5,7 +5,7 @@
"docs/cordis-primer.md": 550,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 800,
"examples/AGENTS.md": 653,
"examples/AGENTS.md": 680,
"packages/AGENTS.md": 450,
"packages/README.md": 660
}

View File

@@ -140,7 +140,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'mode',
title: 'Session-mode policy state',
mode: 'core',
consumers: ['stdio-agent'],
consumers: ['stdio-agent', 'acp'],
note: 'Folds the logged per-agent mode (mode/set), flushes user flips at turn boundaries, and enforces the mode through the assemble filter and the tools/pre-execute gate.',
},
{
@@ -444,6 +444,14 @@ const APP_EXAMPLES = [
config: 'examples/acp-agent/cordis.yml',
summary: 'The ACP demo exposes the same agent spine over JSON-RPC stdio, with no stdout logger and no pre-created agent; clients create sessions through the ACP bridge.',
},
{
id: 'plan-acp',
rel: 'examples/plan-acp-agent/composition.md',
title: 'Plan-Mode ACP Agent App Composition',
label: 'examples/plan-acp-agent',
config: 'examples/plan-acp-agent/cordis.yml',
summary: 'The plan-mode demo composes session modes onto the ACP server: the editor mode picker drives plan mode, and the model exits through the user-reviewed exit_plan_mode tool.',
},
]
type AppExample = typeof APP_EXAMPLES[number]

View File

@@ -42,6 +42,7 @@ import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import ModesService from '@deepseek-ai/dsh-mode'
import WebService from '@deepseek-ai/dsh-web'
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
@@ -137,6 +138,18 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'Registered by the tool registry itself under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the ONLY wire tool; the other registered tools are declared to the model as a generated TypeScript SDK prompt section instead, and a program calls them through port-bridged bindings that dispatch through the ordinary tools/pre-execute → tools/post-execute pipeline, one at a time.',
},
{
pkg: '@deepseek-ai/dsh-mode',
dir: 'mode',
source: 'packages/mode/mode/src/index.ts',
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.userInteraction (execution time, opportunistic)'],
writes: ['tool/call', 'mode/set back to default on an approved review', 'tool/result'],
async mount(ctx) {
await ctx.plugin(ModesService)
},
note:
'exit_plan_mode presents the plan for the user\'s review over the user-interaction seam (approve / keep planning with feedback); approval flips the logged session mode back to default. The assemble filter shows it only while the folded mode is plan.',
},
{
pkg: '@deepseek-ai/dsh-tool-bash',
dir: 'tool-bash',