From 6cd9fefe88a978429cfc8582e8bec9a99102c0a6 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:49:58 +0800 Subject: [PATCH] fix(cli): register web prompt context before boot --- ...-07-28-web-agent-runtime-context.i18n.yaml | 4 +-- .../2026-07-28-web-agent-runtime-context.md | 4 +-- ...2026-07-28-web-agent-runtime-context.zh.md | 4 +-- apps/cli/src/app-cli-entry.ts | 9 ++++-- apps/cli/src/web.ts | 21 +++++++------- apps/cli/tests/web-prompt-context.spec.ts | 29 +++++++++++++++++++ apps/web/tests/scaffold.ts | 4 +-- 7 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 apps/cli/tests/web-prompt-context.spec.ts diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml index f49f2d8314..483bfd9e86 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.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 .agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md -2026-07-28-web-agent-runtime-context.md: 8423f2d5542b7f9c841ecbf354fbea2e25699d13 -2026-07-28-web-agent-runtime-context.zh.md: cfb754bd92836142556638e9195050dfc282146e +2026-07-28-web-agent-runtime-context.md: 449c9d4ba2b144d02dee4b98ae80c86815aec5c1 +2026-07-28-web-agent-runtime-context.zh.md: def1674be5f193739bfb214a24f34590ee075d5f diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md index 8423f2d554..449c9d4ba2 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.md @@ -10,13 +10,13 @@ The shared CLI base configured an empty deployment persona, the Web overlay did ## Decision -The shared Web/headless overlay (`apps/cli/config/web.cordis.yml`) supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`. `dsh web` additionally resolves the harness checkout from the launcher's module URL, installs the existing `harness:source` section, and adds an `app:web-surface` section before serving requests. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other. +The shared Web/headless overlay (`apps/cli/config/web.cordis.yml`) supplies a concise coding-agent persona containing the resolved `{{model}}` and session `{{cwd}}`. `dsh web` additionally resolves the harness checkout from the launcher's module URL, installs the existing `harness:source` section, and adds an `app:web-surface` section before serving requests. The launcher registers that setup before mounting the config tree; its `systemPrompt` injection therefore installs both sections before later prompt consumers such as the agent loop can activate and emit a request header. The [source-checkout/workdir decision](2026-07-30-source-checkout-workdir-distinction.md) owns the source section's wording and its warning not to infer one path from the other. The Web section treats unqualified references to “this page,” “this GUI,” or “this app” as references to the DeepSeek Harness Web GUI. It also states that the browser provides no implicit DOM, route, or screenshot context, so the model can identify the product without claiming visual state it did not receive. The assembled text is logged in `request/header`, preserving the model-visible/logged invariant. ## Verification -The keyless fresh-round-trip Web scenario boots the shipped base plus Web overlay, installs the same launcher context as `dsh web`, runs a real session through the HTTP/SSE application, and snapshots the first four system-prompt sections with source and working-directory paths normalized. The snapshot pins the harness identity, source checkout, Web orientation, and resolved coding-agent persona in request order. +The focused startup-order test registers a later `systemPrompt` consumer and proves that it observes both launcher sections on its first activation. The keyless fresh-round-trip Web scenario boots the shipped base plus Web overlay, registers the same launcher context as `dsh web`, runs a real session through the HTTP/SSE application, and snapshots the first four system-prompt sections with source and working-directory paths normalized. The snapshot pins the harness identity, source checkout, Web orientation, and resolved coding-agent persona in request order. ## Alternatives considered diff --git a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md index cfb754bd92..def1674be5 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-28-web-agent-runtime-context.zh.md @@ -10,13 +10,13 @@ CLI 共享 base 配置了空的部署 persona,Web overlay 没有替换它, ## 决策 -`apps/cli/config/web.cordis.yml` 这份 Web/无头共享 overlay 提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`。`dsh web` 还会根据启动器模块的 URL 解析 harness checkout,安装现有的 `harness:source` 提示词段,并在对外提供请求服务前添加 `app:web-surface` 提示词段。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。 +`apps/cli/config/web.cordis.yml` 这份 Web/无头共享 overlay 提供一段简洁的编码 agent persona,其中包含解析后的 `{{model}}` 与会话 `{{cwd}}`。`dsh web` 还会根据启动器模块的 URL 解析 harness checkout,安装现有的 `harness:source` 提示词段,并在对外提供请求服务前添加 `app:web-surface` 提示词段。启动器会在挂载配置树前注册这项设置;因此,它的 `systemPrompt` 注入会在 agent loop(智能体循环)等后续提示词消费方激活并发出 request header 之前安装这两个提示词段。源码提示词段的措辞,以及其中不得从一条路径推断另一条路径的警告,均由另行记录的[源码 checkout 与工作目录区分决策](2026-07-30-source-checkout-workdir-distinction.md)负责。 Web 提示词段把未限定的「这个页面」「这个 GUI」或「这个应用」解释为 DeepSeek Harness Web GUI。同时,它会明确说明浏览器不会隐式提供 DOM、路由或截图上下文,使模型能够识别产品,但不会声称掌握未收到的视觉状态。组装后的文本会记录在 `request/header` 中,从而保持「模型可见内容必须有日志记录」这一不变量。 ## 验证 -无密钥的 Web fresh-round-trip 场景会启动已交付的 base 与 Web overlay,安装与 `dsh web` 相同的启动器上下文,并通过 HTTP/SSE 应用运行一个真实会话。测试会把源码路径和工作目录规范化,然后对系统提示词的前四个段落生成快照。该快照按请求顺序固定 harness 身份、源码 checkout、Web 界面定位,以及解析后的编码 agent persona。 +聚焦启动顺序的测试会注册一个后续的 `systemPrompt` 消费方,并证明该消费方首次激活时就能观察到启动器的两个提示词段。无密钥的 Web fresh-round-trip 场景会启动已交付的 base 与 Web overlay,注册与 `dsh web` 相同的启动器上下文,并通过 HTTP/SSE 应用运行一个真实会话。测试会把源码路径和工作目录规范化,然后对系统提示词的前四个段落生成快照。该快照按请求顺序固定 harness 身份、源码 checkout、Web 界面定位,以及解析后的编码 agent persona。 ## 考虑过的替代方案 diff --git a/apps/cli/src/app-cli-entry.ts b/apps/cli/src/app-cli-entry.ts index 218df73530..7b182b4073 100644 --- a/apps/cli/src/app-cli-entry.ts +++ b/apps/cli/src/app-cli-entry.ts @@ -155,6 +155,8 @@ export interface AppCLIEntryOptions { workspaceRoot?: string /** Extra authorities for the /api browser-trust fence (`host` or `host:port`), appended to the derived LAN IP literals. */ trustedHosts?: string[] + /** Surface setup registered after Loader installation and before any config-tree entry mounts. */ + prepare?: (ctx: Context) => Promise | void } /** @@ -180,8 +182,8 @@ export class AppCLIEntry { constructor(private readonly options: AppCLIEntryOptions) {} /** - * Run the boot chain: patch composition → Loader include boot (dev row - * before await) → fail-loud triple. + * Run the boot chain: patch composition → Loader installation → surface + * preparation → config-tree boot (dev row before await) → fail-loud triple. * @returns the settled root context and the listening port. */ async run(): Promise<{ ctx: Context; port: number }> { @@ -246,7 +248,7 @@ export class AppCLIEntry { if (telemetryPatch !== undefined) this.patches.push(telemetryPatch) } - /** Shared Loader boot; the dev HMR row mounts before await so the activation audit covers it. */ + /** Shared Loader boot; surface preparation precedes the tree, and the dev HMR row precedes the activation audit. */ private async bootTree(): Promise { // One include of the shared base with every overlay as a sibling patch // list: patches never cross an include boundary, so nesting them would @@ -260,6 +262,7 @@ export class AppCLIEntry { ...this.patches, ] this.ctx = await boot('dsh', resolve(this.options.configPath), patches, async (ctx) => { + await this.options.prepare?.(ctx) if (this.options.dev) await ctx.loader.create({ name: '@deepseek-ai/dsh-client-hmr' }) }) } diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts index 8510f74929..730ccdde93 100644 --- a/apps/cli/src/web.ts +++ b/apps/cli/src/web.ts @@ -23,17 +23,18 @@ const WEB_SURFACE_PROMPT = 'You are interacting with the user through the DeepSe + 'The browser provides no implicit DOM, route, or screenshot context.' /** - * Add the launcher-owned source location and Web-surface orientation after the - * shared config tree settles. The request header logs both sections with every - * model-visible prompt. - * @param ctx - settled Web application context. + * Register the launcher-owned source location and Web-surface orientation + * before the shared config tree mounts. The injection installs both sections + * when `systemPrompt` activates; because it precedes the Loader entries, later + * prompt consumers observe them on their first activation. + * @param ctx - Web root context with Loader installed but no config tree mounted. * @param sourceRoot - absolute checkout root resolved from the launcher module. */ -export function installWebPromptContext(ctx: Context, sourceRoot: string): void { - const systemPrompt = ctx.get('systemPrompt') - if (systemPrompt === undefined) throw new Error('dsh web: systemPrompt service missing after settled boot') - addHarnessSourceSection(ctx, sourceRoot) - systemPrompt.section({ name: 'app:web-surface', order: -98, text: WEB_SURFACE_PROMPT }) +export function prepareWebPromptContext(ctx: Context, sourceRoot: string): void { + ctx.inject(['systemPrompt'], (promptCtx) => { + addHarnessSourceSection(promptCtx, sourceRoot) + promptCtx.systemPrompt.section({ name: 'app:web-surface', order: -98, text: WEB_SURFACE_PROMPT }) + }) } // Display-only mirror of the webserver schema's loopback host: the address the @@ -65,13 +66,13 @@ export async function runWeb( overlayPath: WEB_OVERLAY, ...config !== undefined && { extraOverlayPath: resolveConfigPath(config, undefined) }, dev, + prepare: (ctx) => { prepareWebPromptContext(ctx, SOURCE_ROOT) }, ...host !== undefined && { host }, ...port !== undefined && { port }, ...workspaceRoot !== undefined && { workspaceRoot }, ...trustedHosts !== undefined && { trustedHosts }, }) const { ctx, port: boundPort } = await entry.run() - installWebPromptContext(ctx, SOURCE_ROOT) let exiting = false const shutdown = (code: number): void => { diff --git a/apps/cli/tests/web-prompt-context.spec.ts b/apps/cli/tests/web-prompt-context.spec.ts new file mode 100644 index 0000000000..f19b691ddf --- /dev/null +++ b/apps/cli/tests/web-prompt-context.spec.ts @@ -0,0 +1,29 @@ +import { sep } from 'node:path' +import { Context } from 'cordis' +import { describe, expect, it } from 'vitest' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import { HARNESS_SOURCE_SECTION } from '@deepseek-ai/dsh-app-boot' +import { prepareWebPromptContext } from '../src/web.ts' + +describe('prepareWebPromptContext', () => { + it('installs both sections before a later systemPrompt consumer activates', async () => { + const ctx = new Context() + const sourceRoot = `${sep}opt${sep}harness-src` + let observedNames: string[] | undefined + try { + prepareWebPromptContext(ctx, sourceRoot) + const consumer = ctx.inject(['systemPrompt'], async (promptCtx) => { + const assembly = await promptCtx.systemPrompt.assemble() + observedNames = assembly.sections.map(section => section.name) + }) + + await ctx.plugin(SystemPrompt, { persona: 'You are a coding agent.' }) + await consumer + + expect(observedNames).toContain(HARNESS_SOURCE_SECTION) + expect(observedNames).toContain('app:web-surface') + } finally { + await ctx.fiber.dispose() + } + }) +}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index dfe342b5d4..226471227d 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -53,7 +53,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' // Empty type imports carry the httpServer/agents/sessionPersistence Context merges. import type {} from '@deepseek-ai/dsh-host-webserver' import type {} from '@deepseek-ai/dsh-agent' -import { installWebPromptContext } from '../../cli/src/web.ts' +import { prepareWebPromptContext } from '../../cli/src/web.ts' import { DIST_INDEX, REPO_ROOT, requireDist } from './support.ts' /** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the ACP/TUI suites). */ @@ -301,13 +301,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise