Просмотр исходного кода

Merge branch 'feat/system-prompt-surface-node' into feat/system-prompt-in-history

Tianyi Cui 2 недель назад
Родитель
Сommit
275ff714cf

+ 2 - 2
benchmarks/agent-continuation/README.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 benchmarks/agent-continuation/README.md
-README.md: 5f76b67805a906ea13321ab22eb671d41bfc3190
-README.zh.md: 54d9e1a0e950d14ce5a01568bc9e1cdb61de597a
+README.md: 75f804fc18deb90f1db40093cd52e5df10886b46
+README.zh.md: 047c4988beaebe96e0bd561674e5314139608c41

+ 1 - 1
benchmarks/agent-continuation/README.md

@@ -24,7 +24,7 @@ The test reports all five fresh-process samples and enforces reviewed median bud
 
 ## Measurements
 
-[workload.ts](workload.ts) owns synthetic dimensions. [The Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md) owns timing endpoints, calibration evidence, memory interpretation, and exclusions. The model adapter does not perform provider serialization or network calls; integrated cases run synthetic tool bodies through the real tool-execution pipeline, while the SDK profile variant performs real file reads.
+[workload.ts](workload.ts) owns synthetic dimensions. Its current-generation history reserves an empty system head in the first step before user input, so resumed prompts replace that head without moving historical messages. [The Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.md) owns timing endpoints, calibration evidence, memory interpretation, and exclusions. The model adapter does not perform provider serialization or network calls; integrated cases run synthetic tool bodies through the real tool-execution pipeline, while the SDK profile variant performs real file reads.
 
 ## Dev Note
 

+ 1 - 1
benchmarks/agent-continuation/README.zh.md

@@ -24,7 +24,7 @@
 
 ## 测量
 
-[workload.ts](workload.ts)拥有合成维度。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md)拥有计时终点、校准证据、内存解释和排除项。模型适配器不执行服务商序列化或网络调用;集成用例通过真实工具执行管线运行合成工具体,SDK profile 变体则执行真实文件读取。
+[workload.ts](workload.ts)拥有合成维度。其当前代历史在首个 step 的用户输入之前保留空 system 头节点,因此续聊提示会替换该头节点而不移动历史消息。[Agent Note](../../.agents/notes/implemented/testing/2026-09-06-backend-continuation-performance.zh.md)拥有计时终点、校准证据、内存解释和排除项。模型适配器不执行服务商序列化或网络调用;集成用例通过真实工具执行管线运行合成工具体,SDK profile 变体则执行真实文件读取。
 
 ## Dev Note
 

+ 39 - 0
benchmarks/agent-continuation/synthetic-history.bench.ts

@@ -0,0 +1,39 @@
+/** Current-generation benchmark seeds retain the system head across continuation. */
+import { expect, it } from 'vitest'
+import { createSystemMessage } from '@deepseek-ai/dsh-llm'
+import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
+import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
+import { syntheticHistory as browserHistory } from '../long-session-browser/synthetic-history.ts'
+import { syntheticHistory } from './workload.ts'
+
+const header = { type: 'session', version: SESSION_FORMAT_VERSION, id: 'benchmark-seed-check', createdAt: 1_700_000_000_000, cwd: '/bench', isSeeded: false, delegationDepth: 0 }
+
+for (const [name, generate] of [
+  ['continuation', () => [JSON.stringify(header), ...syntheticHistory(2).map(event => JSON.stringify(event))].join('\n')],
+  ['browser', browserHistory],
+] as const) {
+  it(name + ' seed preserves a protected head when the next prompt replaces it', () => {
+    const events = parseSessionLog(generate())
+    expect(events.slice(0, 4).map(event => event.type)).toEqual(['turn/start', 'step/start', 'system/message', 'user/message'])
+    const session = Session.create(SessionId(header.id), events)
+    const head = session.surface.nodes[0]!
+    expect(session.eventAt(head)).toMatchObject({ type: 'system/message', data: { turn: 1, step: 1, message: { role: 'system', content: [] } } })
+    const history = session.deriveMessages()
+    const turn = events.filter(event => event.type === 'turn/start').length + 1
+    session.append('turn/start', { turn })
+    session.append('step/start', { turn, step: 1 })
+    const replacement = session.append('system/message', {
+      turn, step: 1, message: createSystemMessage('Next synthetic prompt', '@deepseek-ai/dsh-system-prompt'),
+    }, { surfaceOp: { op: 'replace', start: head, end: head }, sourceEventSeqs: [head] })
+    const restored = Session.create(SessionId(header.id), parseSessionLog([
+      JSON.stringify(header), ...session.snapshotEvents().map(event => JSON.stringify(event)),
+    ].join('\n')))
+    expect(restored.surface.nodes[0]).toBe(replacement.seq)
+    expect(restored.deriveMessages()[0]).toMatchObject({ role: 'system', content: [{ type: 'text', text: 'Next synthetic prompt' }] })
+    expect(restored.deriveMessages().slice(1)).toEqual(history)
+    for (const event of events) {
+      if (event.type === 'session/title') expect(session.eventAt(event.data.messageSeqs[0]!)?.type).toBe('user/message')
+      if (event.type === 'tool/result') expect(session.eventAt(event.sourceEventSeqs![0]!)?.type).toBe('tool/call')
+    }
+  })
+}

+ 3 - 0
benchmarks/agent-continuation/workload.ts

@@ -68,6 +68,9 @@ export function syntheticHistory(turns: number): SessionEvent[] {
   for (let turn = 1; turn <= turns; turn++) {
     session.append('turn/start', { turn })
     session.append('step/start', { turn, step: 1 })
+    if (turn === 1) session.append('system/message', {
+      turn, step: 1, message: { id: MessageId('system-head'), role: 'system', content: [], source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' } },
+    }, { surfaceOp: 'append' })
     session.append('user/message', {
       id: MessageId('prompt-' + String(turn)), role: 'user',
       content: [{ type: 'text', text: 'Inspect synthetic module ' + String(turn) }], source: { kind: 'user' },

+ 2 - 2
benchmarks/long-session-browser/README.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 benchmarks/long-session-browser/README.md
-README.md: 6b6777687947bee42337568ad3748e37a563be87
-README.zh.md: 139bd237d45275ea165a3e90f055f8ce507c5c53
+README.md: 6f746522caaabe9e7ce8bd452122512ab6ac8c66
+README.zh.md: 7aced6aa67731cbde8de910b970668e62d643c0d

+ 1 - 1
benchmarks/long-session-browser/README.md

@@ -12,6 +12,6 @@ This reference describes the required Chromium workflow in [long-session.bench.t
 
 Three fresh browser processes and scaffold worlds produce raw samples and median verdicts. Open and paging end after the expected transcript state and two animation frames; this includes a rendering opportunity, not a hardware presentation timestamp. Paging reports every page and gates the median of each sample’s slowest page. Stream reports first visible reply, trusted draft typing, complete reply wall time, and Chromium main-thread task duration. Send lookup is scoped to the composer seat; reply-marker lookups and the input-event text witness read only the latest Assistant step, avoiding repeated whole-history text and accessibility scans. The input witness is installed before Send, and draft typing starts as soon as the first marker is visible, without an extra pre-input animation-frame wait. The actual first input event must observe an unfinished reply; completion waits for the new rendered turn-tail after Host settlement. After measurement, a trusted keystroke after DONE must fail the same overlap assertion. Open, the slowest older page, and first Trajectory use standard-hosted expectations of 900/700/500 ms. Shared 1.25× headroom gives limits of 1125/875/625 ms respectively; stream endpoint overhead budgets are unchanged. Heap after forced GC and DOM counts are diagnostics, not leak budgets.
 
-The fixture contains mixed-language prompts, prose, reasoning, 20 code fences, and 40 synthetic tool results. Every historical Assistant includes a compact stream built by the production accumulator from matching reasoning, text, tool arguments, usage, and finish chunks. No model, tool, external network, recorded Session, or private Harness home supplies its content. Streaming uses 120 text deltas at 16 ms replay pacing through the real composer, agent loop, transport, and persistence.
+The fixture reserves an empty system head before the first user message, with each user message inside its step. It contains mixed-language prompts, prose, reasoning, 20 code fences, and 40 synthetic tool results. Every historical Assistant includes a compact stream built by the production accumulator from matching reasoning, text, tool arguments, usage, and finish chunks. No model, tool, external network, recorded Session, or private Harness home supplies its content. Streaming uses 120 text deltas at 16 ms replay pacing through the real composer, agent loop, transport, and persistence.
 
 The [decision record](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.md) owns calibration, exclusions, and alternatives. The larger [manual diagnostic](../../apps/web/tests/complex-history.perf.ts) remains separate.

+ 1 - 1
benchmarks/long-session-browser/README.zh.md

@@ -12,6 +12,6 @@
 
 三个全新浏览器进程与 scaffold 环境产生原始样本及中位数判定。打开和分页在预期对话状态出现且经过两次动画帧后结束;这包含一次渲染机会,而非硬件显示时间戳。分页报告每一页,并对各样本最慢分页时间的中位数执行预算检查。流式报告首段可见回复、真实草稿键入、完整回复壁钟时间和 Chromium 主线程任务时间。发送控件查找限定在 composer seat;回复标记查找与输入事件文本证据仅读取最新 Assistant step,避免重复扫描全部历史文本与无障碍属性。输入观察器在发送前安装,首个标记可见后立即开始草稿键入,不额外等待输入前动画帧。实际首个输入事件必须观察到未完成的回复;完成测量在 Host 结算后等待新 turn-tail 渲染。测量后,在 DONE 之后发送的真实按键必须无法通过同一个重叠断言。打开、最慢更早分页和首次 Trajectory 使用标准托管预期 900/700/500 ms。共享的 1.25× 余量分别产生 1125/875/625 ms 上限;流式终点的额外开销预算不变。强制 GC 后的 heap 与 DOM 数量仅供诊断,不作为泄漏预算。
 
-fixture(测试前置数据)包含混合语言提示、正文、推理、20 个代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑 stream,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish chunk 构建。其内容不来自模型、工具、外部网络、录制 Session 或私有 Harness 主目录。流式回复以 16 ms 重放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
+fixture(测试前置数据)在首条用户消息前保留空 system 头节点,每条用户消息都位于其 step 内。它包含混合语言提示、正文、推理、20 个代码块和 40 个合成工具结果。每条历史 Assistant 都含紧凑 stream,由生产 accumulator 从匹配的推理、文本、工具参数、usage 和 finish chunk 构建。其内容不来自模型、工具、外部网络、录制 Session 或私有 Harness 主目录。流式回复以 16 ms 重放间隔发送 120 个文本 delta,经过真实输入框、agent loop(智能体循环)、传输与持久化。
 
 [决策记录](../../.agents/notes/implemented/testing/2026-09-06-frontend-performance-budgets.zh.md)拥有校准、排除项与替代方案。更大规模的[手动诊断](../../apps/web/tests/complex-history.perf.ts)保持独立。

+ 5 - 2
benchmarks/long-session-browser/synthetic-history.ts

@@ -1,5 +1,5 @@
 /** Synthetic current-generation history and paced reply for browser measurements. */
-import { createAssistantMessage, createUserMessage, createToolResultMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
+import { createAssistantMessage, createSystemMessage, createUserMessage, createToolResultMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
 import type { StreamChunk } from '@deepseek-ai/dsh-llm'
 import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm/assistant-stream'
 import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
@@ -26,12 +26,15 @@ export function syntheticHistory(): string {
   const session = Session.create(SessionId(SESSION_ID))
   for (let turn = 1; turn <= HISTORY_TURNS; turn++) {
     session.append('turn/start', { turn })
+    session.append('step/start', { turn, step: 1 })
+    if (turn === 1) session.append('system/message', {
+      turn, step: 1, message: createSystemMessage('', '@deepseek-ai/dsh-system-prompt'),
+    }, { surfaceOp: 'append' })
     const user = session.append('user/message', createUserMessage({
       content: [{ type: 'text', text: 'Review synthetic change ' + String(turn) + ': 检查增量渲染。 '.repeat(30) }],
       source: { kind: 'user' },
     }), { surfaceOp: 'append' })
     if (turn === 1) session.append('session/title', { title: TITLE, messageSeqs: [user.seq], source: { kind: 'fallback' } })
-    session.append('step/start', { turn, step: 1 })
     const callId = ToolCallId('synthetic-tool-' + String(turn))
     const tool = turn % 6 === 0
     const code = turn % 12 === 0