mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(daemon): normalize cumulative ACP message chunks (#3333)
* fix(daemon): normalize cumulative acp message chunks - apps/daemon/src/acp.ts - apps/daemon/tests/acp.test.ts - apps/web/src/providers/daemon.ts - apps/web/src/components/DesignSystemFlow.tsx Convert cumulative ACP message snapshots into suffix deltas and keep temporary browser debug instrumentation for trace verification. * chore(web): remove temporary stream debug hooks - apps/web/src/providers/daemon.ts - apps/web/src/components/DesignSystemFlow.tsx Remove the browser debug accumulator after validating the ACP duplication trace.
This commit is contained in:
parent
41b1cd763e
commit
c33641e592
2 changed files with 88 additions and 9 deletions
|
|
@ -457,6 +457,7 @@ export function attachAcpSession({
|
|||
let emittedThinkingStart = false;
|
||||
let emittedFirstTokenStatus = false;
|
||||
let emittedTextChunk = false;
|
||||
let emittedTextBuffer = '';
|
||||
let finished = false;
|
||||
let fatal = false;
|
||||
let aborted = false;
|
||||
|
|
@ -618,16 +619,22 @@ export function attachAcpSession({
|
|||
if (update.sessionUpdate === 'agent_message_chunk') {
|
||||
const text = asObject(update.content)?.text;
|
||||
if (typeof text === 'string' && text.length > 0) {
|
||||
emittedTextChunk = true;
|
||||
if (!emittedFirstTokenStatus) {
|
||||
emittedFirstTokenStatus = true;
|
||||
send('agent', {
|
||||
type: 'status',
|
||||
label: 'streaming',
|
||||
ttftMs: Date.now() - runStartedAt,
|
||||
});
|
||||
const delta = text.startsWith(emittedTextBuffer)
|
||||
? text.slice(emittedTextBuffer.length)
|
||||
: text;
|
||||
if (delta.length > 0) {
|
||||
emittedTextChunk = true;
|
||||
emittedTextBuffer += delta;
|
||||
if (!emittedFirstTokenStatus) {
|
||||
emittedFirstTokenStatus = true;
|
||||
send('agent', {
|
||||
type: 'status',
|
||||
label: 'streaming',
|
||||
ttftMs: Date.now() - runStartedAt,
|
||||
});
|
||||
}
|
||||
send('agent', { type: 'text_delta', delta });
|
||||
}
|
||||
send('agent', { type: 'text_delta', delta: text });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,74 @@ test('attachAcpSession includes image attachments as ACP resource links', () =>
|
|||
});
|
||||
});
|
||||
|
||||
test('attachAcpSession converts cumulative ACP message snapshots into deltas', () => {
|
||||
const child = new FakeAcpChild();
|
||||
const events: Array<{ event: string; payload: unknown }> = [];
|
||||
|
||||
attachAcpSession({
|
||||
child: child as never,
|
||||
prompt: 'describe the project',
|
||||
cwd: '/tmp/od-project',
|
||||
model: null,
|
||||
mcpServers: [],
|
||||
send: (event, payload) => events.push({ event, payload }),
|
||||
});
|
||||
|
||||
writeAcpResult(child, 1, {});
|
||||
writeAcpResult(child, 2, { sessionId: 'session-1' });
|
||||
writeAcpUpdate(child, {
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
content: { text: 'Agent Haven' },
|
||||
});
|
||||
writeAcpUpdate(child, {
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
content: { text: 'Agent Haven — managed AI agents' },
|
||||
});
|
||||
writeAcpUpdate(child, {
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
content: { text: 'Agent Haven — managed AI agents' },
|
||||
});
|
||||
writeAcpResult(child, 3, { usage: { inputTokens: 1, outputTokens: 2 } });
|
||||
|
||||
const textDeltas = events
|
||||
.filter((entry) => entry.event === 'agent' && (entry.payload as { type?: unknown }).type === 'text_delta')
|
||||
.map((entry) => (entry.payload as { delta?: unknown }).delta);
|
||||
|
||||
assert.deepEqual(textDeltas, ['Agent Haven', ' — managed AI agents']);
|
||||
});
|
||||
|
||||
test('attachAcpSession keeps incremental ACP message chunks unchanged', () => {
|
||||
const child = new FakeAcpChild();
|
||||
const events: Array<{ event: string; payload: unknown }> = [];
|
||||
|
||||
attachAcpSession({
|
||||
child: child as never,
|
||||
prompt: 'describe the project',
|
||||
cwd: '/tmp/od-project',
|
||||
model: null,
|
||||
mcpServers: [],
|
||||
send: (event, payload) => events.push({ event, payload }),
|
||||
});
|
||||
|
||||
writeAcpResult(child, 1, {});
|
||||
writeAcpResult(child, 2, { sessionId: 'session-1' });
|
||||
writeAcpUpdate(child, {
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
content: { text: 'Agent Haven' },
|
||||
});
|
||||
writeAcpUpdate(child, {
|
||||
sessionUpdate: 'agent_message_chunk',
|
||||
content: { text: ' — managed AI agents' },
|
||||
});
|
||||
writeAcpResult(child, 3, { usage: { inputTokens: 1, outputTokens: 2 } });
|
||||
|
||||
const textDeltas = events
|
||||
.filter((entry) => entry.event === 'agent' && (entry.payload as { type?: unknown }).type === 'text_delta')
|
||||
.map((entry) => (entry.payload as { delta?: unknown }).delta);
|
||||
|
||||
assert.deepEqual(textDeltas, ['Agent Haven', ' — managed AI agents']);
|
||||
});
|
||||
|
||||
test('attachAcpSession exposes abort and sends session cancel after session creation', () => {
|
||||
const child = new FakeAcpChild();
|
||||
const writes: string[] = [];
|
||||
|
|
@ -328,6 +396,10 @@ function writeAcpResult(child: FakeAcpChild, id: number, result: unknown): void
|
|||
child.stdout.write(`${JSON.stringify({ id, result })}\n`);
|
||||
}
|
||||
|
||||
function writeAcpUpdate(child: FakeAcpChild, update: unknown): void {
|
||||
child.stdout.write(`${JSON.stringify({ method: 'session/update', params: { update } })}\n`);
|
||||
}
|
||||
|
||||
function agentModelStatuses(events: Array<{ event: string; payload: unknown }>): unknown[] {
|
||||
return events
|
||||
.filter((entry) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue