mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
Deduplicate Assistant tool progress updates
This commit is contained in:
@@ -2133,6 +2133,120 @@ describe('useChat', () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('ignores duplicate tool_progress events without refreshing live activity', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
const { getFireEvent } = setupWithEventCapture();
|
||||
const { value: chat, dispose } = withRoot(() => useChat({ sessionId: 's' }));
|
||||
|
||||
await chat.sendMessage('hi');
|
||||
const fire = getFireEvent();
|
||||
|
||||
fire({ type: 'tool_start', data: { id: 'tool-1', name: 'pulse_read', input: '{}' } });
|
||||
vi.setSystemTime(10_100);
|
||||
fire({
|
||||
type: 'tool_progress',
|
||||
data: {
|
||||
id: 'tool-1',
|
||||
name: 'pulse_read',
|
||||
input: '{}',
|
||||
raw_input: '{"command":"ls /dev"}',
|
||||
phase: 'running',
|
||||
message: 'Running command.',
|
||||
},
|
||||
});
|
||||
|
||||
let assistant = chat.messages().find((m) => m.role === 'assistant')!;
|
||||
const firstPendingTool = assistant.pendingTools![0];
|
||||
const firstPendingToolEvent = assistant.streamEvents?.find((e) => e.type === 'pending_tool');
|
||||
expect(firstPendingTool.updatedAt).toBe(10_100);
|
||||
expect(firstPendingToolEvent?.updatedAt).toBe(10_000);
|
||||
expect(firstPendingToolEvent?.pendingTool?.updatedAt).toBe(10_100);
|
||||
|
||||
vi.setSystemTime(20_000);
|
||||
fire({
|
||||
type: 'tool_progress',
|
||||
data: {
|
||||
id: 'tool-1',
|
||||
name: 'pulse_read',
|
||||
input: '{}',
|
||||
raw_input: '{"command":"ls /dev"}',
|
||||
phase: 'running',
|
||||
message: 'Running command.',
|
||||
},
|
||||
});
|
||||
|
||||
assistant = chat.messages().find((m) => m.role === 'assistant')!;
|
||||
const duplicatePendingToolEvents = assistant.streamEvents?.filter(
|
||||
(e) => e.type === 'pending_tool',
|
||||
);
|
||||
expect(assistant.pendingTools).toHaveLength(1);
|
||||
expect(assistant.pendingTools![0]).toBe(firstPendingTool);
|
||||
expect(assistant.pendingTools![0].updatedAt).toBe(10_100);
|
||||
expect(duplicatePendingToolEvents).toHaveLength(1);
|
||||
expect(duplicatePendingToolEvents![0]).toBe(firstPendingToolEvent);
|
||||
expect(duplicatePendingToolEvents![0].updatedAt).toBe(10_000);
|
||||
expect(duplicatePendingToolEvents![0].pendingTool?.updatedAt).toBe(10_100);
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('keeps changed tool_progress updates as fresh live activity', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(30_000);
|
||||
const { getFireEvent } = setupWithEventCapture();
|
||||
const { value: chat, dispose } = withRoot(() => useChat({ sessionId: 's' }));
|
||||
|
||||
await chat.sendMessage('hi');
|
||||
const fire = getFireEvent();
|
||||
|
||||
fire({ type: 'tool_start', data: { id: 'tool-1', name: 'pulse_read', input: '{}' } });
|
||||
vi.setSystemTime(30_100);
|
||||
fire({
|
||||
type: 'tool_progress',
|
||||
data: {
|
||||
id: 'tool-1',
|
||||
name: 'pulse_read',
|
||||
input: '{}',
|
||||
raw_input: '{"command":"ls /dev"}',
|
||||
phase: 'running',
|
||||
message: 'Running command.',
|
||||
},
|
||||
});
|
||||
vi.setSystemTime(30_500);
|
||||
fire({
|
||||
type: 'tool_progress',
|
||||
data: {
|
||||
id: 'tool-1',
|
||||
name: 'pulse_read',
|
||||
input: '{}',
|
||||
raw_input: '{"command":"ls /dev"}',
|
||||
phase: 'running',
|
||||
message: 'Reading output.',
|
||||
},
|
||||
});
|
||||
|
||||
const assistant = chat.messages().find((m) => m.role === 'assistant')!;
|
||||
const pendingToolEvents = assistant.streamEvents?.filter((e) => e.type === 'pending_tool');
|
||||
expect(assistant.pendingTools).toHaveLength(1);
|
||||
expect(assistant.pendingTools![0]).toMatchObject({
|
||||
id: 'tool-1',
|
||||
status: 'running',
|
||||
progress: 'Reading output.',
|
||||
updatedAt: 30_500,
|
||||
});
|
||||
expect(pendingToolEvents).toHaveLength(1);
|
||||
expect(pendingToolEvents![0]).toMatchObject({
|
||||
toolId: 'tool-1',
|
||||
updatedAt: 30_000,
|
||||
pendingTool: expect.objectContaining({
|
||||
id: 'tool-1',
|
||||
progress: 'Reading output.',
|
||||
updatedAt: 30_500,
|
||||
}),
|
||||
});
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('deduplicates repeated tool_start events without regressing live progress', async () => {
|
||||
const { getFireEvent } = setupWithEventCapture();
|
||||
const { value: chat, dispose } = withRoot(() => useChat({ sessionId: 's' }));
|
||||
|
||||
@@ -639,6 +639,14 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
return 'running';
|
||||
};
|
||||
|
||||
const hasSamePendingToolActivity = (current: PendingTool, next: PendingTool): boolean =>
|
||||
current.id === next.id &&
|
||||
current.name === next.name &&
|
||||
current.input === next.input &&
|
||||
current.rawInput === next.rawInput &&
|
||||
current.status === next.status &&
|
||||
current.progress === next.progress;
|
||||
|
||||
const replacePendingToolStreamEvents = (
|
||||
events: StreamDisplayEvent[],
|
||||
resolvedTool: PendingTool,
|
||||
@@ -847,6 +855,7 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
|
||||
let resolvedTool: PendingTool | undefined;
|
||||
let replacedTool = false;
|
||||
let activityChanged = false;
|
||||
const updatedPendingTools: PendingTool[] = [];
|
||||
|
||||
for (const tool of pendingTools) {
|
||||
@@ -858,7 +867,14 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
continue;
|
||||
}
|
||||
replacedTool = true;
|
||||
resolvedTool = mergeTool(tool);
|
||||
const mergedTool = mergeTool(tool);
|
||||
if (hasSamePendingToolActivity(tool, mergedTool)) {
|
||||
resolvedTool = tool;
|
||||
updatedPendingTools.push(tool);
|
||||
continue;
|
||||
}
|
||||
activityChanged = true;
|
||||
resolvedTool = mergedTool;
|
||||
updatedPendingTools.push(resolvedTool);
|
||||
}
|
||||
|
||||
@@ -873,9 +889,14 @@ export function useChat(options: UseChatOptions = {}) {
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
activityChanged = true;
|
||||
updatedPendingTools.push(resolvedTool);
|
||||
}
|
||||
|
||||
if (!activityChanged && resolvedTool) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
return {
|
||||
...msg,
|
||||
streamEvents: resolvedTool
|
||||
|
||||
Reference in New Issue
Block a user