refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/plan/plan-mode/README.md
|
||||
README.md: b4e890f6b11998879119657ee73eb24826c101e1
|
||||
README.zh.md: 784f9f9d98499c0b830836299846872cd49cc1b4
|
||||
README.md: 7171997406ea43487762d9947d07426df400c78a
|
||||
README.zh.md: 5c04cdabc293c9abda6bb5e77f596715ee137c6f
|
||||
|
||||
@@ -12,7 +12,7 @@ Logged, per-agent plan collaboration state with deployment-owned guidance, direc
|
||||
|
||||
## Model and human interactions
|
||||
|
||||
While active, `plan:policy` renders the configured `section`. The plugin always registers `exit_plan_mode`, keeping tool schemas stable across the transition; its execute path accepts only active plan mode and leaves it only after an exact user approval through `ctx.userInteraction`.
|
||||
While active, `plan:policy` renders the configured `section`. The plugin always registers `exit_plan_mode`, keeping tool schemas stable across the transition; its execute path accepts only active plan mode and leaves it only after an exact user approval through `ctx.userQuestions`.
|
||||
|
||||
The review question declares the `plan-review` presentation intent, naming `Approve` as the label that approves it, so a capable UI presents the plan as a decision instead of a generic question; the answer the tool reads is the same either way. A dismissed review — the user closing the request to speak instead — is reported to the model as such, telling it to stay in plan mode and wait for the message; every other review failure keeps the seam's own message.
|
||||
|
||||
@@ -83,7 +83,7 @@ The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-
|
||||
|
||||
#### Token effect
|
||||
|
||||
The stable schema is paid according to ToolRegistry mode, and each plan argument and review result remains in conversation history.
|
||||
The stable schema is paid according to ToolRuntime mode, and each plan argument and review result remains in conversation history.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
## 模型与人类交互
|
||||
|
||||
激活时,`plan:policy` 会渲染已配置的 `section`。插件始终注册 `exit_plan_mode`,使工具 schema 在转换期间保持稳定;其 execute 路径只接受已激活的 plan mode,且只有通过 `ctx.userInteraction` 获得用户明确批准后才退出。
|
||||
激活时,`plan:policy` 会渲染已配置的 `section`。插件始终注册 `exit_plan_mode`,使工具 schema 在转换期间保持稳定;其 execute 路径只接受已激活的 plan mode,且只有通过 `ctx.userQuestions` 获得用户明确批准后才退出。
|
||||
|
||||
评审问题声明 `plan-review` 呈现意图,并指名 `Approve` 为表示批准的标签,因此有能力的 UI 会把计划呈现为一次决定而非通用问题;两种情况下该工具读到的回答完全相同。放弃审阅——用户关闭请求,转而发言——会如实报告给模型,要求它留在 plan mode 中等待那条消息;其余每一种评审失败都保留 seam 自身的消息。
|
||||
|
||||
@@ -83,7 +83,7 @@ You are in plan mode. Explore and design before presenting the complete plan thr
|
||||
|
||||
#### Token 影响
|
||||
|
||||
稳定 schema 的成本取决于 ToolRegistry mode,每次传入的 plan 参数和评审结果都会保留在对话历史中。
|
||||
稳定 schema 的成本取决于 ToolRuntime mode,每次传入的 plan 参数和评审结果都会保留在对话历史中。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -71,7 +71,7 @@
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { UserInteractionError } from '@deepseek-ai/dsh-user-interaction'
|
||||
import { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
|
||||
// Type-only edge: resolves `ctx.commands` for the optional command child.
|
||||
import type {} from '@deepseek-ai/dsh-commands'
|
||||
// Type-only: resolves ctx.sessionProjections for the optional unit child.
|
||||
@@ -56,7 +56,7 @@ declare module '@deepseek-ai/dsh-session/types' {
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
planMode: PlanModeService
|
||||
planMode: PlanModeController
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ function planModeAtLastHeader(events: readonly SessionEvent[]): boolean | undefi
|
||||
* the `plan:policy` section, the `/plan` command, and the stable exit tool.
|
||||
* UIs observe committed flips through `session/event`; there is no live mirror.
|
||||
*/
|
||||
export class PlanModeService extends Service {
|
||||
export class PlanModeController extends Service {
|
||||
static inject = ['tools', 'systemPrompt']
|
||||
|
||||
/** Validated deployment-owned guidance. */
|
||||
@@ -327,9 +327,9 @@ export class PlanModeService extends Service {
|
||||
if (!/^#\s+\S/.test(args.plan.trim())) {
|
||||
throw new Error(`${EXIT_PLAN_MODE} requires a non-empty markdown plan starting with a # heading`)
|
||||
}
|
||||
const interaction = ctx.get('userInteraction')
|
||||
const interaction = ctx.get('userQuestions')
|
||||
if (interaction === undefined) {
|
||||
throw new Error('no user-interaction channel is available to review the plan; ask the user to switch the session mode instead')
|
||||
throw new Error('no user-questions channel is available to review the plan; ask the user to switch the session mode instead')
|
||||
}
|
||||
const answer = await interaction.ask({
|
||||
questions: [{
|
||||
@@ -354,7 +354,7 @@ export class PlanModeService extends Service {
|
||||
// generic channel message names ask_user_question, which the model
|
||||
// never called. An abort (turn cancel, provider teardown) keeps its
|
||||
// own message — there is no user to wait for.
|
||||
if (cause instanceof UserInteractionError && cause.code === 'ASK_CANCELLED') {
|
||||
if (cause instanceof UserQuestionError && cause.code === 'ASK_CANCELLED') {
|
||||
throw new Error('The user dismissed the plan review to speak instead; '
|
||||
+ 'stay in plan mode, stop here, and wait for their message.')
|
||||
}
|
||||
@@ -474,4 +474,4 @@ export class PlanModeService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
export default PlanModeService
|
||||
export default PlanModeController
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LlmService, { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import LlmRuntime, { createUserMessage, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import PlanModeService, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode'
|
||||
import PlanModeController, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
|
||||
const PLAN_CONFIG = { section: 'Test plan mode instructions.' }
|
||||
@@ -21,13 +21,13 @@ const PLAN_CONFIG = { section: 'Test plan mode instructions.' }
|
||||
*/
|
||||
async function harness(adapter: MockAdapter): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
for (const name of ['read', 'write']) {
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
|
||||
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import * as PlanModeInvariant from '@deepseek-ai/dsh-plan-mode/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
async function setup(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
await ctx.plugin(PlanModeInvariant)
|
||||
return ctx
|
||||
}
|
||||
@@ -67,7 +67,7 @@ describe('plan-mode stream invariants', () => {
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('plan/mode', { active: 'plan' as unknown as boolean })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/)
|
||||
})
|
||||
@@ -79,7 +79,7 @@ describe('plan-mode stream invariants', () => {
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('plan/mode', { active: true })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
|
||||
})
|
||||
@@ -88,7 +88,7 @@ describe('plan-mode stream invariants', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
ctx.sessions.create().append('plan/mode', { active: true })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -2,16 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import UserInteractionService, {
|
||||
UserInteractionError, type AskUserQuestionRequest,
|
||||
} from '@deepseek-ai/dsh-user-interaction'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import UserQuestionService, {
|
||||
UserQuestionError, type AskUserQuestionRequest,
|
||||
} from '@deepseek-ai/dsh-user-questions'
|
||||
import CommandRuntime from '@deepseek-ai/dsh-commands'
|
||||
import { CodeRuntime, type CodeRunRequest, type CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import PlanModeService, { EXIT_PLAN_MODE, foldPlanMode, resolveConfig } from '../src/index.ts'
|
||||
import PlanModeController, { EXIT_PLAN_MODE, foldPlanMode, resolveConfig } from '../src/index.ts'
|
||||
import type { PlanModeConfig } from '../src/index.ts'
|
||||
|
||||
const TEST_PLAN_SECTION = 'Test plan mode instructions.'
|
||||
@@ -19,7 +19,7 @@ const PLAN_CONFIG = { section: TEST_PLAN_SECTION } satisfies PlanModeConfig
|
||||
|
||||
/**
|
||||
* Drives the REAL plugin: mounts `dsh-plan-mode` beside real `SystemPrompt` and
|
||||
* `ToolRegistry` services, with fake Agents carrying real `Session`s and a
|
||||
* `ToolRuntime` services, with fake Agents carrying real `Session`s and a
|
||||
* real scoped `agent.ctx` minted through `createScope`.
|
||||
* Request boundaries are simulated by dispatching the real pre-step waterfall
|
||||
* and the following `step/start` session event used by the loop.
|
||||
@@ -68,8 +68,8 @@ function assembleFor(ctx: Context, agent: Agent) {
|
||||
async function setup(config: PlanModeConfig = PLAN_CONFIG): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(PlanModeService, config)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(PlanModeController, config)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -281,8 +281,8 @@ describe('the boundary flush', () => {
|
||||
it('removes the pre-step flush when the plugin fiber is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
const fiber = await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
const agent = await agentWithSession(ctx)
|
||||
openTurn(agent.session)
|
||||
ctx.planMode.set(agent, true)
|
||||
@@ -433,13 +433,13 @@ describe('the soft layer', () => {
|
||||
// Plan guidance does not filter the registry or later assembly additions.
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
|
||||
const final = await next()
|
||||
final.tools = [...final.tools, { name: 'added-later', description: 'added after next()', parameters: {} }]
|
||||
return final
|
||||
})
|
||||
await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
registerNamedTools(ctx, ['read'])
|
||||
const planning = await agentWithSession(ctx, 'planning', { active: true })
|
||||
expect((await assembleFor(ctx, planning)).tools.map(tool => tool.name))
|
||||
@@ -459,9 +459,9 @@ describe('the soft layer', () => {
|
||||
}
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(ToolRuntime, { mode: 'code' })
|
||||
await ctx.plugin(FakeRuntime)
|
||||
await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
registerNamedTools(ctx, ['read', 'write'])
|
||||
const agent = await agentWithSession(ctx, 'agent-1', { active: true })
|
||||
const assembly = await assembleFor(ctx, agent)
|
||||
@@ -480,9 +480,9 @@ describe('the soft layer', () => {
|
||||
}
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'both' })
|
||||
await ctx.plugin(ToolRuntime, { mode: 'both' })
|
||||
await ctx.plugin(FakeRuntime)
|
||||
await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
registerNamedTools(ctx, ['read', 'write'])
|
||||
const agent = await agentWithSession(ctx, 'agent-1', { active: true })
|
||||
const assembly = await assembleFor(ctx, agent)
|
||||
@@ -501,9 +501,9 @@ describe('the soft layer', () => {
|
||||
}
|
||||
const withPlanMode = new Context()
|
||||
await withPlanMode.plugin(SystemPrompt)
|
||||
await withPlanMode.plugin(ToolRegistry, { mode: 'code' })
|
||||
await withPlanMode.plugin(ToolRuntime, { mode: 'code' })
|
||||
await withPlanMode.plugin(FakeRuntime)
|
||||
await withPlanMode.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await withPlanMode.plugin(PlanModeController, PLAN_CONFIG)
|
||||
registerNamedTools(withPlanMode, ['read', 'write'])
|
||||
const agent = await agentWithSession(withPlanMode)
|
||||
const defaultSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
@@ -516,7 +516,7 @@ describe('the soft layer', () => {
|
||||
// with a deployment that does not compose plan mode at all.
|
||||
const bare = new Context()
|
||||
await bare.plugin(SystemPrompt)
|
||||
await bare.plugin(ToolRegistry, { mode: 'code' })
|
||||
await bare.plugin(ToolRuntime, { mode: 'code' })
|
||||
await bare.plugin(FakeRuntime)
|
||||
registerNamedTools(bare, ['read', 'write'])
|
||||
const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
|
||||
@@ -553,7 +553,7 @@ describe('/plan', () => {
|
||||
expect(bare.get('commands')).toBeUndefined()
|
||||
|
||||
const ctx = await setup()
|
||||
await ctx.plugin(CommandService)
|
||||
await ctx.plugin(CommandRuntime)
|
||||
// The `ctx.inject` child mounts asynchronously once `commands` resolves.
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const plainAgent = await agentWithSession(ctx, 'plain-plan-command')
|
||||
@@ -595,7 +595,7 @@ describe('/plan', () => {
|
||||
|
||||
it('leaves active plan mode, cancels a pending entry, and treats inactive exit as idempotent', async () => {
|
||||
const ctx = await setup()
|
||||
await ctx.plugin(CommandService)
|
||||
await ctx.plugin(CommandRuntime)
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const signal = new AbortController().signal
|
||||
|
||||
@@ -633,7 +633,7 @@ describe('/plan', () => {
|
||||
|
||||
it('idle sessions get the immediate-commit copy on both /plan and /plan off', async () => {
|
||||
const ctx = await setup()
|
||||
await ctx.plugin(CommandService)
|
||||
await ctx.plugin(CommandRuntime)
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const signal = new AbortController().signal
|
||||
const agent = await agentWithSession(ctx, 'idle-plan-command')
|
||||
@@ -648,9 +648,9 @@ describe('/plan', () => {
|
||||
it('removes the contributed command when the plan-mode plugin is disposed', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(CommandRuntime)
|
||||
const fiber = await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const agent = await agentWithSession(ctx)
|
||||
expect(ctx.commands.list(agent).map(command => command.name)).toEqual(['plan'])
|
||||
@@ -665,10 +665,10 @@ describe('exit_plan_mode', () => {
|
||||
async function setupWithReview(answer?: { selected: string[]; custom?: string }) {
|
||||
const ctx = await setup()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
const asked: AskUserQuestionRequest[] = []
|
||||
if (answer !== undefined) {
|
||||
ctx.userInteraction.registerProvider({
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: (request) => {
|
||||
asked.push(request)
|
||||
return Promise.resolve({ answers: [{ id: 'plan-review', ...answer }] })
|
||||
@@ -725,12 +725,12 @@ describe('exit_plan_mode', () => {
|
||||
expect(foldPlanMode(agent.session.events)).toBe(true)
|
||||
})
|
||||
|
||||
it('degrades to the manual exit when no user-interaction seam is composed', async () => {
|
||||
it('degrades to the manual exit when no user-questions seam is composed', async () => {
|
||||
const ctx = await setup()
|
||||
const agent = await agentWithSession(ctx, 'agent-1', { active: true })
|
||||
const result = await callExit(ctx, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-interaction channel is available to review the plan; ask the user to switch the session mode instead' }])
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-questions channel is available to review the plan; ask the user to switch the session mode instead' }])
|
||||
expect(foldPlanMode(agent.session.events)).toBe(true)
|
||||
})
|
||||
|
||||
@@ -738,16 +738,16 @@ describe('exit_plan_mode', () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
const result = await callExit(ctx, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-interaction provider is registered' }])
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-questions provider is registered' }])
|
||||
expect(foldPlanMode(agent.session.events)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects review from a runtime-owned agent with consumer-neutral guidance', async () => {
|
||||
const ctx = await setup()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
const ask = vi.fn(async () => ({ answers: [{ id: 'plan-review', selected: ['Approve'] }] }))
|
||||
ctx.userInteraction.registerProvider({ ask })
|
||||
ctx.userQuestions.registerProvider({ ask })
|
||||
const root = await agentWithSession(ctx, 'review-root')
|
||||
const child = await agentWithSession(ctx, 'review-child', { active: true, owner: root })
|
||||
|
||||
@@ -794,13 +794,13 @@ describe('exit_plan_mode', () => {
|
||||
}
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(ToolRuntime, { mode: 'code' })
|
||||
await ctx.plugin(ExitRuntime)
|
||||
await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
const asked: AskUserQuestionRequest[] = []
|
||||
ctx.userInteraction.registerProvider({
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: (request) => {
|
||||
asked.push(request)
|
||||
return Promise.resolve({ answers: [{ id: 'plan-review', selected: ['Approve'] }] })
|
||||
@@ -899,7 +899,7 @@ describe('exit_plan_mode', () => {
|
||||
|
||||
it('treats duplicate review answer items as non-consent', async () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
ctx.userInteraction.registerProvider({
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: () => Promise.resolve({ answers: [
|
||||
{ id: 'plan-review', selected: ['Approve'] },
|
||||
{ id: 'plan-review', selected: ['Keep planning'] },
|
||||
@@ -913,7 +913,7 @@ describe('exit_plan_mode', () => {
|
||||
|
||||
it('a missing answer item reads as keep-planning', async () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
ctx.userInteraction.registerProvider({ ask: () => Promise.resolve({ answers: [] }) })
|
||||
ctx.userQuestions.registerProvider({ ask: () => Promise.resolve({ answers: [] }) })
|
||||
const result = await callExit(ctx, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; revise the plan and present it again.' }])
|
||||
@@ -931,8 +931,8 @@ describe('exit_plan_mode', () => {
|
||||
|
||||
it('reads a dismissed review as the user taking the turn back, not as a failure', async () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
ctx.userInteraction.registerProvider({
|
||||
ask: () => Promise.reject(new UserInteractionError(
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: () => Promise.reject(new UserQuestionError(
|
||||
'the user cancelled ask_user_question', 'ASK_CANCELLED')),
|
||||
})
|
||||
const result = await callExit(ctx, agent)
|
||||
@@ -943,8 +943,8 @@ describe('exit_plan_mode', () => {
|
||||
|
||||
it('leaves every other review failure its own message', async () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
ctx.userInteraction.registerProvider({
|
||||
ask: () => Promise.reject(new UserInteractionError(
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: () => Promise.reject(new UserQuestionError(
|
||||
'ask_user_question was aborted before the user answered', 'ASK_ABORTED')),
|
||||
})
|
||||
const result = await callExit(ctx, agent)
|
||||
@@ -970,12 +970,12 @@ describe('exit_plan_mode', () => {
|
||||
it('fails the call when the plugin is disposed while the review awaits (no phantom exit)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
const fiber = await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
let answer!: (value: { answers: { id: string; selected: string[] }[] }) => void
|
||||
ctx.userInteraction.registerProvider({
|
||||
ctx.userQuestions.registerProvider({
|
||||
ask: () => new Promise((resolve) => { answer = resolve }),
|
||||
})
|
||||
const agent = await agentWithSession(ctx, 'agent-1', { active: true })
|
||||
@@ -994,7 +994,7 @@ describe('exit_plan_mode', () => {
|
||||
|
||||
it('a throwing provider surfaces as the corrective isError and the mode stays plan', async () => {
|
||||
const { ctx, agent } = await setupWithReview()
|
||||
ctx.userInteraction.registerProvider({ ask: () => { throw new Error('review aborted') } })
|
||||
ctx.userQuestions.registerProvider({ ask: () => { throw new Error('review aborted') } })
|
||||
const result = await callExit(ctx, agent)
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Error: review aborted' }])
|
||||
@@ -1034,12 +1034,12 @@ describe('HMR disposal', () => {
|
||||
it('unregisters the service, listeners, prompt section, and stable exit tool with the plugin fiber', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
const fiber = await ctx.plugin(PlanModeController, PLAN_CONFIG)
|
||||
const agent = await agentWithSession(ctx, 'disposed-recovery')
|
||||
openTurn(agent.session)
|
||||
ctx.planMode.set(agent, true)
|
||||
expect(ctx.get('planMode')).toBeInstanceOf(PlanModeService)
|
||||
expect(ctx.get('planMode')).toBeInstanceOf(PlanModeController)
|
||||
expect(ctx.tools.get(EXIT_PLAN_MODE)).toBeDefined()
|
||||
expect((await ctx.systemPrompt.assemble()).sections.map(section => section.name)).toContain('plan:policy')
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import UserQuestionService from '@deepseek-ai/dsh-user-questions'
|
||||
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import PlanModeController from '@deepseek-ai/dsh-plan-mode'
|
||||
|
||||
interface Bench {
|
||||
ctx: Context
|
||||
@@ -32,11 +32,11 @@ async function harness(withPlanMode: boolean): Promise<Bench> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SystemPrompt, { persona: '' })
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
if (withPlanMode) await ctx.plugin(PlanModeService, { section: 'plan policy' })
|
||||
if (withPlanMode) await ctx.plugin(PlanModeController, { section: 'plan policy' })
|
||||
const session = ctx.sessions.create()
|
||||
ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
|
||||
return {
|
||||
@@ -115,7 +115,7 @@ describe('plan projection unit', () => {
|
||||
|
||||
it('drops the key when the plan-mode fiber unloads (HMR safety)', async () => {
|
||||
const bench = await harness(false)
|
||||
const fiber = await bench.ctx.plugin(PlanModeService, { section: 'plan policy' })
|
||||
const fiber = await bench.ctx.plugin(PlanModeController, { section: 'plan policy' })
|
||||
expect(bench.values().plan).toEqual({ active: false, pending: false })
|
||||
await fiber.dispose()
|
||||
expect('plan' in bench.values()).toBe(false)
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/user-interaction"
|
||||
"path": "../../interaction/user-questions"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/commands"
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "../../session/session-projection"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user