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
1 change: 1 addition & 0 deletions src/supervisor/agents/acp/canonicalMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
getDetachedSubAgentToolCallIdForNotification,
PORACODE_ACP_NEW_ASSISTANT_ITEM_META_KEY,
PORACODE_ACP_PARENT_TOOL_CALL_ID_META_KEY,
PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY,
PORACODE_ACP_TOP_LEVEL_TOOL_CALL_META_KEY,
} from "./canonicalMapping/subagents";
export { mapAcpSessionUpdate } from "./canonicalMapping/dispatch";
Expand Down
31 changes: 21 additions & 10 deletions src/supervisor/agents/acp/canonicalMapping/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isUpdateTopicTool,
PORACODE_ACP_DETACHED_SUBAGENT_META_KEY,
PORACODE_ACP_NEW_ASSISTANT_ITEM_META_KEY,
PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY,
removeActiveSubAgent,
selectActiveSubAgentForToolCall,
tagSubAgentChildStarts,
Expand Down Expand Up @@ -377,6 +378,7 @@ export function mapAcpSessionUpdate(
const hasOpenSubAgentContent =
item.isSubAgent &&
isTerminal &&
updateMeta?.[PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY] !== true &&
(state.openAssistantItemId !== undefined || state.openReasoningItemId !== undefined);
const subAgentProgress =
item.isSubAgent && !hasOpenSubAgentContent
Expand All @@ -401,20 +403,29 @@ export function mapAcpSessionUpdate(
if (isTerminal && item.isSubAgent) {
events.push(...closeOpenContentItems(state));
}
events.push({
const parentEvent: RuntimeEvent = {
type: isTerminal ? "item.completed" : "item.updated",
threadId,
itemId: item.itemId,
payload: emittedPayload,
});
if (subAgentProgress?.text) {
events.push(...buildSubAgentProgressEvents(state, item, subAgentProgress.text, isTerminal));
} else if (isTerminal && item.subAgentProgressItemId) {
events.push({
type: "item.completed",
threadId,
itemId: item.subAgentProgressItemId,
});
};
const progressEvents = subAgentProgress?.text
? buildSubAgentProgressEvents(state, item, subAgentProgress.text, isTerminal)
: isTerminal && item.subAgentProgressItemId
? [
{
type: "item.completed" as const,
threadId,
itemId: item.subAgentProgressItemId,
},
]
: [];
// Child transcript events must precede the terminal parent event. The
// runtime router drains buffered children when the parent completes.
if (isTerminal) {
events.push(...progressEvents, parentEvent);
} else {
events.push(parentEvent, ...progressEvents);
}
if (isTerminal) {
state.toolCallItems.delete(toolCall.toolCallId);
Expand Down
1 change: 1 addition & 0 deletions src/supervisor/agents/acp/canonicalMapping/subagents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const PORACODE_ACP_TOP_LEVEL_TOOL_CALL_META_KEY = "poracodeTopLevelToolCa
export const PORACODE_ACP_DETACHED_SUBAGENT_META_KEY = "poracodeDetachedSubAgent";
export const PORACODE_ACP_DETACHED_SUBAGENT_ACTIVITY_META_KEY = "poracodeDetachedSubAgentActivity";
export const PORACODE_ACP_NEW_ASSISTANT_ITEM_META_KEY = "poracodeNewAssistantItem";
export const PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY = "poracodeSynthesizeSubAgentResult";

export function buildSubAgentProgress(
toolCall: {
Expand Down
7 changes: 7 additions & 0 deletions src/supervisor/agents/acp/subagentCoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PORACODE_ACP_DETACHED_SUBAGENT_META_KEY,
PORACODE_ACP_NEW_ASSISTANT_ITEM_META_KEY,
PORACODE_ACP_PARENT_TOOL_CALL_ID_META_KEY,
PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY,
PORACODE_ACP_TOP_LEVEL_TOOL_CALL_META_KEY,
} from "./canonicalMapping/subagents";

Expand Down Expand Up @@ -52,6 +53,7 @@ export interface AcpSubagentCompletionInput {
childOutput?: string;
parentReply?: string;
terminalMeta?: Record<string, unknown>;
synthesizeResultProgress?: boolean;
}

export interface AcpSubagentCoordinator {
Expand Down Expand Up @@ -170,6 +172,7 @@ export function createAcpSubagentCoordinator(): AcpSubagentCoordinator {
...(input.childOutput ? { childOutput: input.childOutput } : {}),
...(input.parentReply ? { parentReply: input.parentReply } : {}),
...(input.terminalMeta ? { terminalMeta: input.terminalMeta } : {}),
...(input.synthesizeResultProgress ? { synthesizeResultProgress: true } : {}),
});
forgetCall(toolCallId);
return notifications;
Expand Down Expand Up @@ -248,6 +251,7 @@ function createAcpSubagentCompletionNotifications(input: {
childOutput?: string;
parentReply?: string;
terminalMeta?: Record<string, unknown>;
synthesizeResultProgress?: boolean;
}): SessionNotification[] {
const notifications: SessionNotification[] = [];
if (input.childOutput) {
Expand Down Expand Up @@ -281,6 +285,9 @@ function createAcpSubagentCompletionNotifications(input: {
_meta: {
...input.terminalMeta,
[PORACODE_ACP_DETACHED_SUBAGENT_ACTIVITY_META_KEY]: input.toolCallId,
...(input.synthesizeResultProgress
? { [PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY]: true }
: {}),
},
}),
);
Expand Down
99 changes: 98 additions & 1 deletion src/supervisor/agents/qwen/acpTransform.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { describe, expect, it } from "vitest";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import {
createAcpMapperState,
mapAcpSessionUpdate,
PORACODE_ACP_DETACHED_SUBAGENT_ACTIVITY_META_KEY,
PORACODE_ACP_GOAL_META_KEY,
PORACODE_ACP_PARENT_TOOL_CALL_ID_META_KEY,
PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY,
PORACODE_ACP_TOP_LEVEL_TOOL_CALL_META_KEY,
} from "../acp/canonicalMapping";
import { createQwenAcpSessionBridge, createQwenAcpSessionUpdateTransform } from "./acpTransform";
Expand Down Expand Up @@ -200,7 +203,7 @@ describe("createQwenAcpSessionUpdateTransform", () => {
type: "content",
content: {
type: "text",
text: "Background agent launched successfully.\nagentId: Explore-abcd1234 (internal ID)",
text: "Background agent launched successfully.\ntask_id: Explore-abcd1234 (internal ID)",
},
},
],
Expand Down Expand Up @@ -270,10 +273,104 @@ describe("createQwenAcpSessionUpdateTransform", () => {
_meta: {
usage: { totalTokens: 42 },
[PORACODE_ACP_DETACHED_SUBAGENT_ACTIVITY_META_KEY]: "agent-bg",
[PORACODE_ACP_SYNTHESIZE_SUBAGENT_RESULT_META_KEY]: true,
},
});
});

it("maps the Qwen 0.21 background result into a child transcript before completion", () => {
const bridge = createQwenAcpSessionBridge();
const state = createAcpMapperState("qwen-thread");
const mapUpdate = (update: Record<string, unknown>) =>
mapAcpSessionUpdate(bridge.sessionUpdateTransform(note(update)), state);

const started = mapUpdate({
sessionUpdate: "tool_call",
toolCallId: "agent-real",
title: "Agent: Inspect ACP mapping",
status: "pending",
_meta: { toolName: "agent" },
});
const parentItemId = (
started.find((event) => event.type === "item.started") as {
itemId: string;
}
).itemId;

mapUpdate({
sessionUpdate: "tool_call_update",
toolCallId: "agent-real",
status: "completed",
content: [
{
type: "content",
content: {
type: "text",
text: "Background agent launched successfully.\ntask_id: Explore-real123 (internal ID)",
},
},
],
rawOutput: {
status: "background",
subagentName: "Explore",
taskDescription: "Inspect ACP mapping",
},
_meta: { toolName: "agent" },
});
mapUpdate({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: 'Background agent "Explore" completed.' },
_meta: {
source: "background_notification",
backgroundTask: { taskId: "Explore-real123", status: "completed" },
},
});
mapUpdate({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "The background agent found the mapper." },
_meta: {
source: "background_notification_response",
backgroundTask: { taskId: "Explore-real123", status: "completed" },
},
});

const boundary = bridge.extensionSessionUpdateTransform("_qwencode/end_turn", {
sessionId: "qwen-session",
reason: "end_turn",
source: "background_notification",
});
const terminalEvents = mapAcpSessionUpdate(bridge.sessionUpdateTransform(boundary!), state);
const childStart = terminalEvents.find(
(event) => event.type === "item.started" && event.parentItemId === parentItemId,
);
expect(childStart).toMatchObject({
type: "item.started",
itemType: "assistant_message",
parentItemId,
});
expect(terminalEvents).toContainEqual(
expect.objectContaining({
type: "content.delta",
itemId: childStart && "itemId" in childStart ? childStart.itemId : "",
stream: "assistant_text",
delta: "The background agent found the mapper.",
}),
);
const childCompletedIndex = terminalEvents.findIndex(
(event) =>
event.type === "item.completed" &&
childStart &&
"itemId" in childStart &&
event.itemId === childStart.itemId,
);
const parentCompletedIndex = terminalEvents.findIndex(
(event) => event.type === "item.completed" && event.itemId === parentItemId,
);
expect(childCompletedIndex).toBeGreaterThanOrEqual(0);
expect(parentCompletedIndex).toBeGreaterThan(childCompletedIndex);
expect(state.activeSubAgents).toEqual([]);
});

it("uses Qwen's real background end-turn extension as the terminal boundary", () => {
const bridge = createQwenAcpSessionBridge();
const transform = bridge.sessionUpdateTransform;
Expand Down
7 changes: 5 additions & 2 deletions src/supervisor/agents/qwen/acpTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import {
} from "../acp/subagentCoordinator";
import type { AcpSessionUpdateTransform } from "../base";

const BACKGROUND_AGENT_ID_RE = /\bagentId:\s*([^\s(]+)/iu;
// Qwen 0.21 renamed the launch result field from `agentId` to `task_id`.
// Accept both so resumed sessions created by older CLIs still correlate.
const BACKGROUND_TASK_ID_RE = /\b(?:agentId|task_id):\s*([^\s(]+)/iu;
const QWEN_BACKGROUND_END_TURN_METHOD = "_qwencode/end_turn";

export interface QwenAcpSessionBridge {
Expand Down Expand Up @@ -203,6 +205,7 @@ export function createQwenAcpSessionUpdateTransform(): AcpSessionUpdateTransform
status: "completed",
...(result ? { result } : {}),
terminalMeta: plainRecord(update._meta),
synthesizeResultProgress: true,
})
.at(-1) ?? notification;
const pausedGoal = pausedGoalUpdate();
Expand Down Expand Up @@ -244,7 +247,7 @@ function extractBackgroundTaskId(content: unknown): string | undefined {
for (const entry of content) {
const contentRecord = plainRecord(plainRecord(entry).content);
const text = readString(contentRecord, "text");
const taskId = text ? BACKGROUND_AGENT_ID_RE.exec(text)?.[1] : undefined;
const taskId = text ? BACKGROUND_TASK_ID_RE.exec(text)?.[1] : undefined;
if (taskId) return taskId;
}
return undefined;
Expand Down