From 4d0c0478f643333b1ab9c3c98192190fd974ba0f Mon Sep 17 00:00:00 2001 From: fz Date: Fri, 24 Jul 2026 17:09:05 +0800 Subject: [PATCH] fix(web): defer default mode reminder --- .../2026-07-24-web-plan-mode.i18n.yaml | 4 +- .../feature/2026-07-24-web-plan-mode.md | 6 +- .../feature/2026-07-24-web-plan-mode.zh.md | 6 +- apps/web/tests/smoke-real.e2e.ts | 55 +++++-------------- packages/client/ui-plan/README.md | 6 +- packages/client/ui-plan/package.json | 2 - packages/client/ui-plan/src/index.ts | 12 +--- .../client/ui-plan/tests/node-plugin.spec.ts | 40 +------------- pnpm-lock.yaml | 6 -- 9 files changed, 30 insertions(+), 107 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml index 24a944d6e6..c64bf50899 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.i18n.yaml @@ -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 -2026-07-24-web-plan-mode.md: 395ef2a3afcdac53c5d97b5555c2ffd635497db2 -2026-07-24-web-plan-mode.zh.md: 21727052c284805b38d411e0215af876b2986e35 +2026-07-24-web-plan-mode.md: a2e68e4d31498da06c6dce0966f255bce2e7dc4e +2026-07-24-web-plan-mode.zh.md: 7cdf53d963e38cb76d63b89384ff7f1473b384fc diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md index 395ef2a3af..a2e68e4d31 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.md @@ -14,7 +14,7 @@ Plan selection is not a stop command. The plan service deliberately queues the l `@deepseek-ai/dsh-client-ui-plan` is one Web feature plugin with lifecycle-coupled host and browser entries. Its host entry mounts `@deepseek-ai/dsh-plan-mode` with the Web product's complete planning policy. Its browser entry registers `PlanModeControl` into the session-scoped `conversation.composer.controls` list slot. The `dsh web` roster selects the package once; plugin discovery loads the browser entry while the same roster mount supplies the host behavior. -The policy is product-owned configuration at this composition boundary. Its active section tells the model to inspect before planning, avoid mutation while planning, resolve discoverable facts without asking, make the plan decision-complete, and submit it as the only final `exit_plan_mode` call. Its complementary default section explicitly says that the session is no longer in plan mode, permits normal implementation work, and forbids calling the still-registered exit tool. This current-state statement prevents earlier conversation text about plan mode from confusing a later default request. The plan package continues to own logged state, boundary timing, the active prompt-section decision, the stable exit-tool schema, and review semantics. The Web plugin does not copy those mechanisms. +The policy is product-owned configuration at this composition boundary. It tells the model to inspect before planning, avoid mutation while planning, resolve discoverable facts without asking, make the plan decision-complete, and submit it as the only final `exit_plan_mode` call. The plan package continues to own logged state, boundary timing, prompt-section activation, the stable exit-tool schema, and review semantics. The Web plugin does not copy any of those mechanisms. `ui-conversation` owns and renders the new additive controls slot to the left of the primary composer action. It provides no business payload; entries receive the standard session kit. Whole-composer replacements remain on the separate selector-routed `conversation.composer` chain, so a pending question replaces the InputBar and its controls without either feature importing the other. @@ -42,7 +42,7 @@ Approval queues inactive mode for the next step; it does not rewrite the current ## Product composition and evidence -Fixture mode implements the same pending and boundary behavior in memory so browser acceptance tests exercise the assembled product without a key. The keyless browser flow selects Plan, commits it with a prompt, selects Default during generation, stops without losing the pending target, and commits Default with the next prompt. A file snapshot records each user-visible state. A real `dsh web` process with a mock provider additionally proves that the roster mounts plan mode, the state RPC reports capability, and both exact-mode Web policies reach their corresponding provider requests alongside workspace instructions. +Fixture mode implements the same pending and boundary behavior in memory so browser acceptance tests exercise the assembled product without a key. The keyless browser flow selects Plan, commits it with a prompt, selects Default during generation, stops without losing the pending target, and commits Default with the next prompt. A file snapshot records each user-visible state. A real `dsh web` process with a mock provider additionally proves that the roster mounts plan mode, the state RPC reports capability, and the active Web policy reaches the provider request alongside workspace instructions. ## Alternatives considered @@ -58,4 +58,4 @@ Fixture mode implements the same pending and boundary behavior in memory so brow ## Consequences -The Web product now exposes the same plan interaction model as the current terminal and ACP compositions while retaining its plugin boundaries. Selecting the feature adds one host policy/tool owner and one browser slot entry; removing its fiber removes both. The model tool catalog stays stable across mode changes, but the active Web mode section changes at a plan boundary and therefore changes the request prefix. Plan mode remains guidance, not an execution sandbox: deployments that require enforced read-only planning still compose the independent sandbox and approval policies. +The Web product now exposes the same plan interaction model as the current terminal and ACP compositions while retaining its plugin boundaries. Selecting the feature adds one host policy/tool owner and one browser slot entry; removing its fiber removes both. The model tool catalog stays stable across mode changes, but the active system-prompt section changes at a plan boundary and therefore changes the request prefix. Plan mode remains guidance, not an execution sandbox: deployments that require enforced read-only planning still compose the independent sandbox and approval policies. diff --git a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md index 21727052c2..7cdf53d963 100644 --- a/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-07-24-web-plan-mode.zh.md @@ -14,7 +14,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p `@deepseek-ai/dsh-client-ui-plan` 是一个 Web 功能插件,包含生命周期耦合的宿主入口和浏览器入口。宿主入口使用 Web 产品的完整 plan 策略挂载 `@deepseek-ai/dsh-plan-mode`。浏览器入口则把 `PlanModeControl` 注册到会话作用域的 `conversation.composer.controls` 列表槽。`dsh web` 的插件清单只需选择该包一次:插件发现机制会加载浏览器入口,同一次清单挂载则提供宿主行为。 -该策略是此组合边界上由产品拥有的配置。其激活段要求模型先检查再规划、规划期间避免修改、无需询问即可自行查明能够发现的事实、使 plan 包含完成决策所需的全部信息,并把 plan 作为唯一且最终的 `exit_plan_mode` 调用提交。与之配套的默认段会明确说明会话已不处于 plan mode,允许正常实施工作,并禁止调用仍保持注册的退出工具。此项当前状态声明可避免对话中较早出现的 plan mode 文本干扰之后的默认模式请求。plan 包(package)继续拥有已记录的状态、边界时序、提示词段激活判定、稳定的退出工具 schema 和评审语义。Web 插件不会复制这些机制。 +该策略是此组合边界上由产品拥有的配置。它要求模型先检查再规划、规划期间避免修改、无需询问即可自行查明能够发现的事实、使 plan 包含完成决策所需的全部信息,并把 plan 作为唯一且最终的 `exit_plan_mode` 调用提交。plan 包(package)继续拥有已记录的状态、边界时序、提示词段激活、稳定的退出工具 schema 和评审语义。Web 插件不会复制其中任何机制。 `ui-conversation` 拥有并渲染新增的可叠加控件槽,其位置在 composer 主操作左侧。该槽不提供业务载荷;各入口接收标准会话注入项。替换整个 composer 的功能仍走另一条由选择器路由的 `conversation.composer` 链,因此待处理的问题会替换 InputBar 及其控件,两个功能均无需导入对方。 @@ -42,7 +42,7 @@ Web 宿主可以通过[可选会话 RPC 契约](../architecture/2026-07-24-web-p ## 产品组合与证据 -fixture(测试前置数据)模式在内存中实现相同的待生效与边界行为,因此浏览器验收测试无需密钥即可覆盖组合后的产品。无密钥浏览器流程依次选择「计划」、通过提示词提交该模式、在生成期间选择「默认」、停止生成且不丢失待生效目标,再通过下一条提示词提交「默认」。文件快照记录每个用户可见状态。另一个使用 mock 提供方的真实 `dsh web` 进程还证明:插件清单会挂载 plan mode,状态 RPC 会报告该功能,并且 Web 针对两种确切模式的策略会随工作区指令分别进入对应的提供方请求。 +fixture(测试前置数据)模式在内存中实现相同的待生效与边界行为,因此浏览器验收测试无需密钥即可覆盖组合后的产品。无密钥浏览器流程依次选择「计划」、通过提示词提交该模式、在生成期间选择「默认」、停止生成且不丢失待生效目标,再通过下一条提示词提交「默认」。文件快照记录每个用户可见状态。另一个使用 mock 提供方的真实 `dsh web` 进程还证明:插件清单会挂载 plan mode,状态 RPC 会报告该功能,并且激活的 Web 策略会随工作区指令一同进入提供方请求。 ## 考虑过的替代方案 @@ -58,4 +58,4 @@ fixture(测试前置数据)模式在内存中实现相同的待生效与边 ## 后果 -Web 产品现在会在保留插件边界的同时,公开与现有终端和 ACP(Agent Client Protocol)产品组合相同的 plan 交互模型。选择该功能会增加一个宿主策略/工具所有者和一个浏览器槽入口;移除其 fiber 会同时移除二者。模型工具目录在模式切换期间保持稳定,但激活的 Web 模式段会在 plan 边界发生变化,因此请求前缀也会改变。Plan mode 仍是引导机制,而非执行沙箱:需要强制只读规划的部署仍需组合彼此独立的沙箱策略和审批策略。 +Web 产品现在会在保留插件边界的同时,公开与现有终端和 ACP(Agent Client Protocol)产品组合相同的 plan 交互模型。选择该功能会增加一个宿主策略/工具所有者和一个浏览器槽入口;移除其 fiber 会同时移除二者。模型工具目录在模式切换期间保持稳定,但激活的系统提示词段会在 plan 边界发生变化,因此请求前缀也会改变。Plan mode 仍是引导机制,而非执行沙箱:需要强制只读规划的部署仍需组合彼此独立的沙箱策略和审批策略。 diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 98e2369174..4fb3087f53 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -193,30 +193,22 @@ describe('dsh web keyless CLI smoke', () => { } }) - it('injects workspace instructions and the exact Web mode policy into provider requests', async () => { + it('injects workspace instructions and the active Web plan policy into the provider request', async () => { requireDist() const workspace = mkdtempSync(join(tmpdir(), 'dsh-web-workspace-')) mkdirSync(join(workspace, '.git')) writeFileSync(join(workspace, 'AGENTS.md'), 'web-workspace-context-probe\n') - type ProviderRequest = { messages?: { role?: string; content?: string }[] } - let resolveDefaultRequest!: (request: ProviderRequest) => void - let resolvePlanRequest!: (request: ProviderRequest) => void - const defaultRequest = new Promise((resolve) => { - resolveDefaultRequest = resolve - }) - const planRequest = new Promise((resolve) => { - resolvePlanRequest = resolve + let resolveProviderRequest!: (request: { messages?: { role?: string; content?: string }[] }) => void + const providerRequest = new Promise<{ messages?: { role?: string; content?: string }[] }>((resolve) => { + resolveProviderRequest = resolve }) const provider = createServer((request, response) => { let body = '' request.setEncoding('utf8') request.on('data', (chunk: string) => { body += chunk }) request.on('end', () => { - const parsed = JSON.parse(body) as ProviderRequest - const system = parsed.messages?.find(message => message.role === 'system')?.content ?? '' - if (system.includes('You are in default mode, not plan mode.')) resolveDefaultRequest(parsed) - if (system.includes('Stay in plan mode until exit_plan_mode succeeds')) resolvePlanRequest(parsed) + resolveProviderRequest(JSON.parse(body) as { messages?: { role?: string; content?: string }[] }) response.writeHead(200, { 'content-type': 'text/event-stream' }) response.end([ 'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}', @@ -249,43 +241,26 @@ describe('dsh web keyless CLI smoke', () => { try { const baseUrl = await waitForReadyLine(child) const created = await rpc<{ sessionId: string }>(baseUrl, 'session.create', {}) - await rpc<{ accepted: true }>(baseUrl, 'session.prompt', { - sessionId: created.sessionId, - mode: 'queue', - content: [{ type: 'text', text: 'default request' }], - planMode: false, - }) - const capturedDefault = await Promise.race([ - defaultRequest, - new Promise((_resolve, reject) => { - setTimeout(() => { reject(new Error('default provider request not received in 10s')) }, 10_000).unref() - }), - ]) - const workspaceMessage = capturedDefault.messages?.find(message => - message.role === 'user' && message.content?.includes('web-workspace-context-probe')) - const defaultSystem = capturedDefault.messages?.find(message => message.role === 'system') - expect(defaultSystem?.content).toContain('You are in default mode, not plan mode.') - expect(defaultSystem?.content).toContain('Do not call exit_plan_mode in default mode.') - expect(defaultSystem?.content).not.toContain('Stay in plan mode until exit_plan_mode succeeds') expect(await rpc(baseUrl, 'session.setPlanMode', { sessionId: created.sessionId, active: true, - })).toMatchObject({ pending: true }) + })).toEqual({ active: false, pending: true }) await rpc<{ accepted: true }>(baseUrl, 'session.prompt', { sessionId: created.sessionId, mode: 'queue', - content: [{ type: 'text', text: 'plan request' }], + content: [{ type: 'text', text: 'go' }], planMode: true, }) - const capturedPlan = await Promise.race([ - planRequest, + const captured = await Promise.race([ + providerRequest, new Promise((_resolve, reject) => { - setTimeout(() => { reject(new Error('plan provider request not received in 10s')) }, 10_000).unref() + setTimeout(() => { reject(new Error('provider request not received in 10s')) }, 10_000).unref() }), ]) - const planSystem = capturedPlan.messages?.find(message => message.role === 'system') - expect(planSystem?.content).toContain('Stay in plan mode until exit_plan_mode succeeds') - expect(planSystem?.content).toContain('Do not edit or write files') - expect(planSystem?.content).not.toContain('You are in default mode, not plan mode.') + const workspaceMessage = captured.messages?.find(message => + message.role === 'user' && message.content?.includes('web-workspace-context-probe')) + const systemMessage = captured.messages?.find(message => message.role === 'system') + expect(systemMessage?.content).toContain('Stay in plan mode until exit_plan_mode succeeds') + expect(systemMessage?.content).toContain('Do not edit or write files') expect(await rpc(baseUrl, 'session.planMode', { sessionId: created.sessionId })) .toEqual({ active: true }) expect(workspaceMessage).toMatchInlineSnapshot(` diff --git a/packages/client/ui-plan/README.md b/packages/client/ui-plan/README.md index a82648b86d..2a2a4478cd 100644 --- a/packages/client/ui-plan/README.md +++ b/packages/client/ui-plan/README.md @@ -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's active and default mode policies; 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 policy; 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 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. +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. #### KV Cache effect -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. +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. ## Known Limitations and Deferred Work diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index 81ab46c604..709e88677d 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -45,11 +45,9 @@ "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:^", diff --git a/packages/client/ui-plan/src/index.ts b/packages/client/ui-plan/src/index.ts index 214376be64..7face1a04d 100644 --- a/packages/client/ui-plan/src/index.ts +++ b/packages/client/ui-plan/src/index.ts @@ -3,7 +3,7 @@ * logged plan-mode service with the Web product's planning policy. */ import type { Context } from 'cordis' -import PlanModeService, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode' +import PlanModeService from '@deepseek-ai/dsh-plan-mode' /** Host services required by plan mode. */ export const inject = ['tools', 'systemPrompt'] @@ -21,20 +21,10 @@ 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 }) } diff --git a/packages/client/ui-plan/tests/node-plugin.spec.ts b/packages/client/ui-plan/tests/node-plugin.spec.ts index 434f4df7fb..3669d61195 100644 --- a/packages/client/ui-plan/tests/node-plugin.spec.ts +++ b/packages/client/ui-plan/tests/node-plugin.spec.ts @@ -4,9 +4,7 @@ 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 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' +import { WEB_PLAN_SECTION, apply, inject } from '../src/index.ts' let ctx: Context | undefined @@ -29,45 +27,13 @@ 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:default-policy', text: '' }), - expect.objectContaining({ name: 'plan:policy', text: '' }), - ])) + .toEqual(expect.arrayContaining([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)) - .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) + .not.toContain('plan:policy') }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d568f40fe..2f0e61a225 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -686,9 +686,6 @@ importers: specifier: ^18.2.0 version: 18.3.1 devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../../core/agent '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -698,9 +695,6 @@ importers: '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt