diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index c3fad836d..d6b0f9377 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -553,6 +553,61 @@ describe("PiRpcAgentSession", () => { ]); }); + test("streams Pi task calls as sub-agent cards with lifecycle status", async () => { + const { pi, session, events } = await createSession(); + const fakeSession = pi.latestSession(); + + await session.startTurn("delegate this"); + fakeSession.emit({ + type: "tool_execution_start", + toolCallId: "task-1", + toolName: "task", + args: { + agent: "explore", + task: "Trace the Pi provider tool mapper", + }, + }); + fakeSession.emit({ + type: "tool_execution_end", + toolCallId: "task-1", + toolName: "task", + result: { content: [{ type: "text", text: "Found the mapper." }] }, + isError: false, + }); + fakeSession.finishTurn(); + + await events.nextTurnCompletion(); + + expect(events.timelineItems()).toEqual([ + { + type: "tool_call", + callId: "task-1", + name: "task", + status: "running", + detail: { + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "", + }, + error: null, + }, + { + type: "tool_call", + callId: "task-1", + name: "task", + status: "completed", + detail: { + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "Found the mapper.", + }, + error: null, + }, + ]); + }); + test("keeps one generated message id when Pi omits message start and response id", async () => { const { pi, session, events } = await createSession(); const fakeSession = pi.latestSession(); diff --git a/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts index dd2213ed1..224e86176 100644 --- a/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts @@ -136,6 +136,37 @@ describe("Pi tool call mapper", () => { }); }); + test("maps task calls to sub-agent detail while running", () => { + const toolCall = parseToolArgs("task", { + agent: "explore", + task: "Trace the Pi provider tool mapper", + }); + + expect(mapToolDetail(toolCall, null)).toEqual({ + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "", + }); + }); + + test("maps completed subagent calls with task input to sub-agent detail", () => { + const toolCall = parseToolArgs("subagent", { + agent: "reviewer", + task: "Review the Pi mapper change", + }); + const result = parseToolResult({ + content: [{ type: "text", text: "The mapper change preserves provider status." }], + }); + + expect(mapToolDetail(toolCall, result)).toEqual({ + type: "sub_agent", + subAgentType: "reviewer", + description: "Review the Pi mapper change", + log: "The mapper change preserves provider status.", + }); + }); + test("normalizes Pi MCP proxy calls from requested tool args while running", () => { const toolCall = parseToolArgs("mcp", { tool: "paseo_list_models", diff --git a/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts b/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts index b53ed3993..499627377 100644 --- a/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts @@ -340,6 +340,10 @@ export function resolveToolCallName(toolCall: PiTrackedToolCall, result?: PiTool export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult): ToolCallDetail { const parsedResult = result ?? null; + if (isTaskToolCall(toolCall)) { + return mapTaskToolDetail(toolCall.args, parsedResult); + } + switch (toolCall.kind) { case "bash": { const summary = resolveToolCallOutput(parsedResult); @@ -388,6 +392,30 @@ export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult } } +function isTaskToolCall(toolCall: PiTrackedToolCall): boolean { + if (toolCall.toolName === "task") { + return true; + } + if (toolCall.toolName !== "subagent" || !isRecord(toolCall.args)) { + return false; + } + return ( + toolCall.args.action === undefined && + (readNonEmptyString(toolCall.args.agent) !== undefined || + readNonEmptyString(toolCall.args.task) !== undefined) + ); +} + +function mapTaskToolDetail(args: unknown, result: PiToolResult): ToolCallDetail { + const argRecord = isRecord(args) ? args : {}; + return { + type: "sub_agent", + subAgentType: readNonEmptyString(argRecord.agent), + description: readNonEmptyString(argRecord.task), + log: extractTextFromToolResult(result)?.trim() ?? "", + }; +} + function mapWriteToolDetail(args: WriteToolInput, result: PiToolResult): ToolCallDetail { if (result && typeof result !== "string" && result.details && "xdev" in result.details) { const xdev = XdevExecuteDetailsSchema.safeParse(result.details.xdev);