fix(web): make plan transitions admission-safe
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @deepseek-ai/dsh-client-ui-plan
|
||||
|
||||
Web plan-mode feature with two lifecycle-coupled halves. The node entry mounts `@deepseek-ai/dsh-plan-mode` with the Web product policy; the browser entry contributes a session-scoped selector to `conversation.composer.controls`.
|
||||
Web plan-mode feature with two lifecycle-coupled halves. The node entry mounts `@deepseek-ai/dsh-plan-mode` with the Web product's active and default mode policies; the browser entry contributes a session-scoped selector to `conversation.composer.controls`.
|
||||
|
||||
The selector distinguishes unavailable capability (`planMode === null`), committed mode (`active`), and the target queued for the next model-request boundary (`pending`, including `pending: false`). Selecting a mode never cancels a running turn. It remains available while generation is running, disables only during its own RPC, and displays the host-confirmed pending target until a logged `plan/mode` event commits it. The transparent native select mirrors keyboard focus onto the visible chip and carries a dynamic accessible description of the committed and pending modes.
|
||||
|
||||
@@ -8,11 +8,11 @@ The model exits plan mode through the stable `exit_plan_mode` tool. Its plan rev
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through `@deepseek-ai/dsh-plan-mode`; that package owns policy activation, the exit-tool schema and rendering, logged state, and request-boundary transitions, while this package supplies the Web composition's section text.
|
||||
Indirectly, through complementary Web system-prompt sections: active mode supplies the planning policy, while default mode explicitly states that the session is not planning, permits normal implementation work, and tells the model not to call the still-registered `exit_plan_mode` tool; `@deepseek-ai/dsh-plan-mode` continues to own the exit-tool schema and rendering, logged state, and request-boundary transitions.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Entering or leaving plan mode changes the active system-prompt section and therefore the request prefix. The stable exit-tool registration avoids an additional tool-catalog shape change across the same transition.
|
||||
Entering or leaving plan mode swaps the active Web mode section and therefore changes the request prefix. The stable exit-tool registration avoids an additional tool-catalog shape change across the same transition.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* logged plan-mode service with the Web product's planning policy.
|
||||
*/
|
||||
import type { Context } from 'cordis'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import PlanModeService, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode'
|
||||
|
||||
/** Host services required by plan mode. */
|
||||
export const inject = ['tools', 'systemPrompt']
|
||||
@@ -21,10 +21,20 @@ Make the plan decision-complete: state the goal and success criteria; group impl
|
||||
|
||||
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.`
|
||||
|
||||
/** Web product-owned guidance rendered while plan mode is inactive. */
|
||||
export const WEB_DEFAULT_SECTION = 'You are in default mode, not plan mode. Follow the user\'s request normally, including implementing changes when requested. Do not call exit_plan_mode in default mode. It remains in the tool catalog only for request-cache stability and becomes valid only after the user switches this session to plan mode. This current mode statement overrides earlier conversational text that described the session as being in plan mode.'
|
||||
|
||||
/**
|
||||
* Mount plan mode for hosts that selected the Web plan plugin.
|
||||
* @param ctx - Host context carrying tools and systemPrompt.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'plan:default-policy',
|
||||
order: 50,
|
||||
text: context => context.agent !== undefined && !foldPlanMode(context.agent.session.events)
|
||||
? WEB_DEFAULT_SECTION
|
||||
: '',
|
||||
})
|
||||
ctx.plugin(PlanModeService, { section: WEB_PLAN_SECTION })
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import { EXIT_PLAN_MODE } from '@deepseek-ai/dsh-plan-mode'
|
||||
import { WEB_PLAN_SECTION, apply, inject } from '../src/index.ts'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { WEB_DEFAULT_SECTION, WEB_PLAN_SECTION, apply, inject } from '../src/index.ts'
|
||||
|
||||
let ctx: Context | undefined
|
||||
|
||||
@@ -27,13 +29,45 @@ describe('ui-plan node plugin', () => {
|
||||
expect(WEB_PLAN_SECTION).toContain('Stay in plan mode until exit_plan_mode succeeds')
|
||||
expect(WEB_PLAN_SECTION).toContain('Do not edit or write files')
|
||||
expect(WEB_PLAN_SECTION).toContain('Make exit_plan_mode the only and final tool call')
|
||||
expect(WEB_DEFAULT_SECTION).toContain('default mode, not plan mode')
|
||||
expect(WEB_DEFAULT_SECTION).toContain('Do not call exit_plan_mode in default mode')
|
||||
expect((await ctx.systemPrompt.assemble()).sections)
|
||||
.toEqual(expect.arrayContaining([expect.objectContaining({ name: 'plan:policy', text: '' })]))
|
||||
.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'plan:default-policy', text: '' }),
|
||||
expect.objectContaining({ name: 'plan:policy', text: '' }),
|
||||
]))
|
||||
|
||||
await feature.dispose()
|
||||
expect(ctx.get('planMode')).toBeUndefined()
|
||||
expect(ctx.tools.get(EXIT_PLAN_MODE)).toBeUndefined()
|
||||
expect((await ctx.systemPrompt.assemble()).sections.map(section => section.name))
|
||||
.not.toContain('plan:policy')
|
||||
.toEqual(expect.not.arrayContaining(['plan:default-policy', 'plan:policy']))
|
||||
})
|
||||
|
||||
it('states the exact Web collaboration mode at every agent assembly', async () => {
|
||||
ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
const feature = ctx.plugin({ inject: [...inject], apply })
|
||||
await feature.await()
|
||||
|
||||
const events: SessionEvent[] = []
|
||||
const agent = { session: { events } } as unknown as Agent
|
||||
let sections = (await ctx.systemPrompt.assemble({ agent })).sections
|
||||
expect(sections.find(section => section.name === 'plan:default-policy')?.text)
|
||||
.toBe(WEB_DEFAULT_SECTION)
|
||||
expect(sections.find(section => section.name === 'plan:policy')?.text).toBe('')
|
||||
|
||||
events.push({
|
||||
type: 'plan/mode',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: { active: true },
|
||||
})
|
||||
sections = (await ctx.systemPrompt.assemble({ agent })).sections
|
||||
expect(sections.find(section => section.name === 'plan:default-policy')?.text).toBe('')
|
||||
expect(sections.find(section => section.name === 'plan:policy')?.text)
|
||||
.toBe(WEB_PLAN_SECTION)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user