mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Keep both subagent cards labeled when Claude launches two background agents together (#6268)
* fix(server): keep a subagent card labeled when two background agents launch together The daemon's stream coalescer kept only the last update per tool call inside its 60 ms window. When Claude launches two background Agent calls in one message, the first child's task_started arrives a few ms after the second call starts streaming, so its labeled card is buffered and then overwritten by the unlabeled completed Agent update. The coalescer now folds updates with the same merge the timeline projection applies, so a labeled detail is not replaced by an unknown one. Closes #6252 * refactor(server): compute the merged tool call before building the coalescer entry --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -723,6 +723,57 @@ describe("AgentStreamCoalescer", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps a known tool call detail when a later update in the window has none", () => {
|
||||
const { coalescer, flushes } = createHarness();
|
||||
primeLeadingEdge(coalescer, flushes);
|
||||
|
||||
coalescer.handle(
|
||||
"agent-1",
|
||||
timeline({
|
||||
type: "tool_call",
|
||||
callId: "agent-call",
|
||||
name: "Task",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "general-purpose",
|
||||
description: "List files",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
coalescer.handle(
|
||||
"agent-1",
|
||||
timeline({
|
||||
type: "tool_call",
|
||||
callId: "agent-call",
|
||||
name: "Agent",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: { type: "unknown", input: { description: "List files" }, output: null },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(flushes.map((flush) => flush.item)).toEqual([
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "agent-call",
|
||||
name: "Agent",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "general-purpose",
|
||||
description: "List files",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves mixed text and tool call arrival order within a flush", async () => {
|
||||
const { coalescer, flushes } = createHarness();
|
||||
primeLeadingEdge(coalescer, flushes);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentProvider, AgentStreamEvent, AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import { mergeToolCallItems } from "./timeline-projection.js";
|
||||
|
||||
export const AGENT_STREAM_COALESCE_DEFAULT_WINDOW_MS = 60;
|
||||
|
||||
@@ -183,9 +184,16 @@ export class AgentStreamCoalescer {
|
||||
}
|
||||
|
||||
const existingIndex = buffer.toolCallEntryIndexes.get(event.item.callId);
|
||||
const existing = existingIndex !== undefined ? buffer.entries[existingIndex] : undefined;
|
||||
// Merge rather than replace, so an update that carries less (an unknown detail after a
|
||||
// labeled one) cannot erase what the timeline would have kept had both been flushed.
|
||||
let item = event.item;
|
||||
if (existing?.kind === "tool_call") {
|
||||
item = mergeToolCallItems(existing.item, event.item);
|
||||
}
|
||||
const entry: PendingToolCallEntry = {
|
||||
kind: "tool_call",
|
||||
item: event.item,
|
||||
item,
|
||||
provider: event.provider,
|
||||
...(event.turnId !== undefined ? { turnId: event.turnId } : {}),
|
||||
};
|
||||
|
||||
@@ -87,7 +87,8 @@ function mergeToolCallDetail(existing: ToolCallDetail, incoming: ToolCallDetail)
|
||||
return incoming;
|
||||
}
|
||||
|
||||
function mergeToolCallItems(
|
||||
/** Fold a later update for the same tool call into the one it follows. */
|
||||
export function mergeToolCallItems(
|
||||
existing: Extract<AgentTimelineItem, { type: "tool_call" }>,
|
||||
incoming: Extract<AgentTimelineItem, { type: "tool_call" }>,
|
||||
): Extract<AgentTimelineItem, { type: "tool_call" }> {
|
||||
|
||||
Reference in New Issue
Block a user