From 3469de58eb178e7662ece6ce061a26a7b7eaacd8 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:26:54 +0800 Subject: [PATCH] fix(ui-trajectory): consume prompt changes once --- .../src/client/trajectory-snapshot-builder.ts | 11 +++- .../tests/snapshot-builder.spec.ts | 62 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/client/ui-trajectory/tests/snapshot-builder.spec.ts diff --git a/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts b/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts index 717172c9d8..585975b0aa 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts @@ -46,6 +46,7 @@ function headerFor( function applyHeader( request: Extract, header: TrajectoryRequestHeaderState | undefined, + includeChange: boolean, ): Extract { return header === undefined ? request @@ -53,7 +54,7 @@ function applyHeader( ...request, prompt: header.prompt, requestConfig: header.prompt.config, - ...(header.change === undefined ? {} : { promptChange: header.change }), + ...(includeChange && header.change !== undefined ? { promptChange: header.change } : {}), } } @@ -150,6 +151,7 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder< const boundaries: { seq: number; time: number }[] = [] const turnEndings: { turn: number; time: number; error?: string }[] = [] const callSchemas = new Map() + const consumedPromptChanges = new Set() let partial: TrajectorySnapshot['partial'] = null const runningCalls: TrajectorySnapshot['runningCalls'][number][] = [] @@ -163,7 +165,12 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder< const header = data.request === undefined ? undefined : headerFor(data.request, headers) if (data.node !== undefined) finalized.push(withRequestConfig(data.node, header?.prompt)) if (data.partial !== null) partial = data.partial - if (data.request !== undefined) requests.push(applyHeader(data.request, header)) + if (data.request !== undefined) { + const includeChange = header?.change !== undefined + && !consumedPromptChanges.has(header.seq) + requests.push(applyHeader(data.request, header, includeChange)) + if (includeChange) consumedPromptChanges.add(header.seq) + } continue } if (data.kind === 'tool') { diff --git a/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts b/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts new file mode 100644 index 0000000000..87e484a433 --- /dev/null +++ b/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest' +import type { RequestView } from '@deepseek-ai/dsh-client-runtime/client' +import type { TrajectoryConversationViewNode } from '../src/client/trajectory-contract.ts' +import { TrajectorySnapshotBuilder } from '../src/client/trajectory-snapshot-builder.ts' + +function assistantRequest(startSeq: number, step: number): Extract { + return { + purpose: 'assistant', + startSeq, + turn: 1, + step, + startedAt: startSeq, + completedAt: startSeq + 1, + status: 'complete', + } +} + +describe('TrajectorySnapshotBuilder', () => { + it('inherits one request header across requests without repeating its prompt change', () => { + const prompt = { + config: { provider: 'test', model: 'test' }, + system: 'one initial prompt', + tools: [], + } + const nodes: TrajectoryConversationViewNode[] = [ + { + key: 'header', + kind: 'trajectory-request-header', + id: '2', + target: 'trajectory', + anchorSeq: 2, + data: { + kind: 'request-header', + header: { + seq: 2, + time: 2, + prompt, + change: { seq: 2, time: 2, kind: 'initial' }, + location: { kind: 'session' }, + }, + }, + }, + ...[assistantRequest(3, 1), assistantRequest(5, 2)].map(request => ({ + key: `assistant:${request.step}`, + kind: 'trajectory-assistant-step', + id: `1:${request.step}`, + target: 'trajectory' as const, + anchorSeq: request.startSeq, + data: { kind: 'assistant' as const, partial: null, request }, + })), + ] + + const snapshot = new TrajectorySnapshotBuilder().replace({ nodes }) + + expect(snapshot.requests.map(request => request.purpose === 'assistant' + ? request.prompt?.system + : undefined)).toEqual(['one initial prompt', 'one initial prompt']) + expect(snapshot.requests.map(request => request.purpose === 'assistant' + ? request.promptChange?.kind + : undefined)).toEqual(['initial', undefined]) + }) +})