Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ describe("parallel sub-agent nesting", () => {
expect(counts.subagents).toBe(4);
});

it("never renders in-flight subagent children as active top-level rows", () => {
// Devin child markers carry subagent_context and never get their own
// terminal update, so they stay isComplete=false until the owning Agent
// resolves them. They must not leak into the top-level active-tool rows;
// that is what painted every narrative element as currently running.
const tools: ToolCall[] = [
mkTool({ id: "call-spawn", toolName: "Agent", startedAt: 1000, isComplete: true }),
mkTool({ id: "agent-1", toolName: "Agent", startedAt: 1001, isComplete: false, parentToolCallId: "call-spawn" }),
mkTool({ id: "c1", toolName: "Read", startedAt: 2000, isComplete: false, parentToolCallId: "agent-1" }),
mkTool({ id: "c2", toolName: "Grep", startedAt: 2100, isComplete: false, parentToolCallId: "agent-1" }),
mkTool({ id: "c3", toolName: "Read", startedAt: 2200, isComplete: false, parentToolCallId: "agent-1" }),
];

const { items } = buildNarrativeItems({
toolCalls: tools,
hooks: [],
thoughtSegments: [],
streamingText: "",
isAgentRunning: true,
});

expect(items.filter((i) => i.type === "active-tool")).toEqual([]);
const agentItem = requiredSubagentItem(
items.find((i) => i.type === "subagent" && i.toolCall.id === "agent-1"),
);
expect(agentItem.lifecycle).toBe("started");
expect(agentItem.children.map((c) => c.id)).toEqual(["c1", "c2", "c3"]);
});

it("treats empty-string parentToolCallId as top-level (cannot be a real parent id)", () => {
// Defensive: if a bad event ever surfaces parentToolCallId: "" it must
// be treated as no parent at all. Pre-fix, "" went into childrenMap[""]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,33 @@ describe("mapDevinAcpSessionNotification", () => {
expect(state.pendingSubagentCallIds).toEqual([]);
});

it("attributes parallel subagent_started updates to their run_subagent calls in spawn order", () => {
const state = createDevinAcpTurnState();
const runSubagent = (toolCallId: string) =>
notification(
toolCall(toolCallId, {
kind: "other",
_meta: { "cognition.ai/inferenceToolName": "run_subagent" },
}),
);
const subagentStarted = (agentId: string) =>
notification({
sessionUpdate: "tool_call_update",
toolCallId: agentId,
status: "in_progress",
_meta: { "cognition.ai/subagent_started": { task: "work", title: "Work" } },
});

mapDevinAcpSessionNotification(runSubagent("call-a"), THREAD, state);
mapDevinAcpSessionNotification(runSubagent("call-b"), THREAD, state);

const first = mapDevinAcpSessionNotification(subagentStarted("agent-1"), THREAD, state);
const second = mapDevinAcpSessionNotification(subagentStarted("agent-2"), THREAD, state);

expect(first[0]).toMatchObject({ toolCallId: "agent-1", parentToolCallId: "call-a" });
expect(second[0]).toMatchObject({ toolCallId: "agent-2", parentToolCallId: "call-b" });
});

it("copies task to description on run_subagent markers so narrative extractors find it", () => {
const state = createDevinAcpTurnState();
const events = mapDevinAcpSessionNotification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,10 @@ function subagentStartedEvents(
threadId: string,
state: DevinAcpTurnState,
): AgentEvent[] {
const parentToolCallId = state.pendingSubagentCallIds[
state.pendingSubagentCallIds.length - 1
];
// Devin emits subagent_started in spawn order, so pair it with the oldest
// unresolved run_subagent call. `last` would attach every parallel agent to
// the most recent call, hiding their subtrees under the wrong parent row.
const parentToolCallId = state.pendingSubagentCallIds.shift();
if (parentToolCallId) state.subagentParentByAgentId.set(toolCallId, parentToolCallId);
const task = typeof started.task === "string" ? started.task : undefined;
const title = typeof started.title === "string" ? started.title : undefined;
Expand Down Expand Up @@ -423,8 +424,6 @@ function subagentCompletedEvents(
): AgentEvent[] {
const parentToolCallId = state.subagentParentByAgentId.get(toolCallId);
if (parentToolCallId) {
const idx = state.pendingSubagentCallIds.indexOf(parentToolCallId);
if (idx >= 0) state.pendingSubagentCallIds.splice(idx, 1);
state.subagentParentByAgentId.delete(toolCallId);
}
state.accumulator.toolStartTimes.delete(toolCallId);
Expand Down
Loading