feat(mode): the session-mode core — logged per-agent policy state (@deepseek-ai/dsh-mode)

Plan mode's stage 1 (RFC 2026-07-07-plan-mode): a new packages/mode/ group
with one product package owning the mode/set SessionEventMap vocabulary
(log-only, non-surface, whole-value replace), the pure foldMode, and the
ctx.modes service (list/get/set). User flips are pending intents flushed
at turn/start / step/end — turn enclosure makes an idle append illegal —
with one coalesced context/message notice when the flushed mode differs
from what the last logged request header told the model; a folded mode
the config no longer defines reads as default plus one boundary notice.

Enforcement is two covering layers: a system-prompt/assemble wrapper
filters the RETURNED assembly's tools to the mode's allowlist (and shows
exit_plan_mode IFF the folded mode is plan) beside the mode:policy
section at order 50, and a tools/pre-execute gate denies deny-by-default
against the same allowlist, judging by the logged mode only. The default
mode is the absence of policy — assemblies stay byte-identical to a
no-dsh-mode deployment.

AgentOptions.mode (declaration-merged) seeds a child's initial mode
through the same flush on agent/created; the stdio app gains /mode
(print/switch, never sent to the model) over an opportunistic
ctx.get('modes'). Config is an explicit resolve step: the built-in plan
definition (read-only allowlist; bash/subagent excluded until the
sandbox family lands) merges unless overridden, 'default' as a key
throws at load, unknown names throw at set() time.
This commit is contained in:
kingwl
2026-07-10 01:38:39 +08:00
parent ab8dc1ec72
commit 63ced3e0e2
26 changed files with 1179 additions and 5 deletions
+28
View File
@@ -28,6 +28,9 @@ import {
type AskUserQuestionOption,
type AskUserQuestionRequest,
} from '@deepseek-ai/dsh-user-interaction'
// Type-only edge: makes `ctx.get('modes')` resolve the ModesService type when
// @deepseek-ai/dsh-mode is composed; the runtime read stays opportunistic.
import type {} from '@deepseek-ai/dsh-mode'
export const name = 'ui-stdio'
export const inject = ['agents', 'userInteraction']
@@ -357,6 +360,31 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
ctx.logger.error('ui-stdio: agent "%s" is not running', agentId)
return
}
if (text === '/mode' || text.startsWith('/mode ')) {
// A command line, never sent to the model: print or switch the session
// mode. The switch is a pending intent the mode service flushes at the
// next turn boundary (dsh-mode's turn-enclosure contract).
const modes = ctx.get('modes')
if (modes === undefined) {
output.write('session modes are not composed in this deployment\n> ')
return
}
const target = text.slice('/mode'.length).trim()
if (target === '') {
const { current, pending } = modes.get(agent)
const pendingNote = pending === undefined ? '' : ` (pending: ${pending})`
output.write(`mode: ${current}${pendingNote} — available: ${modes.list().join(', ')}\n> `)
return
}
try {
modes.set(agent, target)
output.write(`mode → ${target} (applies from the next turn)\n> `)
} catch (error) {
// ModesService.set throws only Error (its unknown-name validation).
output.write(`${(error as Error).message}\n> `)
}
return
}
submittedWork = true
if (agent.status === 'running') {
agent.steer([{ type: 'text', text }])