feat(mode): submit optional slash-command message
This commit is contained in:
@@ -6,4 +6,4 @@ Session modes: named, logged, per-agent collaboration states, with **plan mode**
|
||||
|---|---|---|
|
||||
| `mode/` | `mode/set` vocabulary + fold, the `ctx.modes` service (list/get/set with the turn-boundary flush), the `mode:policy` guidance section, and the model-facing `exit_plan_mode` review tool | `ctx.modes` |
|
||||
|
||||
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery. The deployment supplies plan instructions through Cordis config, while `exit_plan_mode` remains registered in every mode to keep the request tool catalog stable. UIs read flips off `session/event`; the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker, and a composed [command registry](../ui/commands) gains one entry command per configured definition (`/plan` for the required definition). Design: [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md).
|
||||
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery. The deployment supplies plan instructions through Cordis config, while `exit_plan_mode` remains registered in every mode to keep the request tool catalog stable. UIs read flips off `session/event`; the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker, and a composed [command registry](../ui/commands) gains one entry command per configured definition (`/plan [message]` for the required definition, with an optional next-step message). Design: [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md).
|
||||
|
||||
@@ -22,7 +22,7 @@ There is no creation-time mode option: a UI (or a plugin) selects through `set()
|
||||
|
||||
## Per-mode slash commands
|
||||
|
||||
When a command registry (`@deepseek-ai/dsh-commands`) is composed, each configured definition contributes its own entry command to interactive front doors. The required definition supplies `/plan`; a further `review` definition supplies `/review`. These commands accept no arguments, record the switch through `set()`, and report that it applies from the next turn. `default` is the absence of a definition and contributes no command. Without a commands service the child never mounts and nothing else changes.
|
||||
When a command registry (`@deepseek-ai/dsh-commands`) is composed, each configured definition contributes its own entry command to interactive front doors. The required definition supplies `/plan [message]`; a further `review` definition supplies `/review [message]`. Each command records its named switch through `set()`; when the optional message is non-empty, the handler trims it and passes it to `agent.steer()` so a running agent receives it in its next step and an idle agent starts a new turn. `default` is the absence of a definition and contributes no command. Without a commands service the child never mounts and nothing else changes.
|
||||
|
||||
Definition names must match `/^[a-z][a-z0-9_-]*$/u`, the shared mode/command subset; config fails at load before a definition can become selectable but undispatchable.
|
||||
|
||||
@@ -78,6 +78,20 @@ Each qualifying transition adds one short conversation message once. The dynamic
|
||||
|
||||
The notice itself is append-only conversation growth. A real mode transition also changes the earlier order-50 section, so that section remains the limiting cache boundary.
|
||||
|
||||
### Per-mode command message
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The command name and result remain in the direct command plane. A non-empty optional suffix is trimmed and submitted as one ordinary user text block through `agent.steer()` after the mode selection, so the resulting step sees the selected mode.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The command itself adds no tokens. An optional message has the same history and token cost as submitting that text separately.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The optional message is append-only conversation growth. Entering the mode still changes the order-50 system-prompt section for the affected step.
|
||||
|
||||
### Exit tool schema and review exchange
|
||||
|
||||
#### What the model sees
|
||||
|
||||
@@ -301,12 +301,12 @@ export class ModesService extends Service {
|
||||
commandCtx.commands.register({
|
||||
name: mode,
|
||||
description: `Enter ${mode} mode`,
|
||||
input: { hint: '[message]' },
|
||||
handler: ({ agent, rawInput }) => {
|
||||
if (rawInput.trim() !== '') {
|
||||
return { kind: 'error', text: `Usage: /${mode}` }
|
||||
}
|
||||
const message = rawInput.trim()
|
||||
this.set(agent, mode)
|
||||
return { kind: 'success', text: `Entering ${mode} mode (applies from the next turn).` }
|
||||
if (message !== '') agent.steer([{ type: 'text', text: message }])
|
||||
return { kind: 'success', text: `Entering ${mode} mode (applies from the next step).` }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -610,21 +610,23 @@ describe('per-mode slash commands', () => {
|
||||
// The `ctx.inject` child mounts asynchronously once `commands` resolves.
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const agent = await agentWithSession(ctx)
|
||||
const steer = vi.fn()
|
||||
;(agent as unknown as { steer: typeof steer }).steer = steer
|
||||
expect(ctx.commands.list(agent)).toEqual([
|
||||
{ name: 'plan', description: 'Enter plan mode' },
|
||||
{ name: 'review', description: 'Enter review mode' },
|
||||
{ name: 'plan', description: 'Enter plan mode', input: { hint: '[message]' } },
|
||||
{ name: 'review', description: 'Enter review mode', input: { hint: '[message]' } },
|
||||
])
|
||||
|
||||
const signal = new AbortController().signal
|
||||
expect(await ctx.commands.execute(agent, '/mode', signal)).toBeUndefined()
|
||||
expect(await ctx.commands.execute(agent, '/plan later', signal))
|
||||
.toEqual({ kind: 'error', text: 'Usage: /plan' })
|
||||
const plan = await ctx.commands.execute(agent, '/plan', signal)
|
||||
expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next turn).' })
|
||||
const plan = await ctx.commands.execute(agent, '/plan draft the migration ', signal)
|
||||
expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' })
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: PLAN_MODE })
|
||||
expect(steer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }])
|
||||
const review = await ctx.commands.execute(agent, '/review', signal)
|
||||
expect(review).toEqual({ kind: 'success', text: 'Entering review mode (applies from the next turn).' })
|
||||
expect(review).toEqual({ kind: 'success', text: 'Entering review mode (applies from the next step).' })
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: 'review' })
|
||||
expect(steer).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('removes every contributed command when the mode plugin is disposed', async () => {
|
||||
|
||||
Reference in New Issue
Block a user