fix(pi): render delegated tasks with lifecycle status (#2891)
Pi left task and direct subagent tool calls in the unknown detail shape, so clients rendered raw request and result payloads. Normalize execution calls at the provider boundary so the existing task card owns their running and terminal states. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
parent
5012607643
commit
94b57f34ad
@@ -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 () => {
|
test("keeps one generated message id when Pi omits message start and response id", async () => {
|
||||||
const { pi, session, events } = await createSession();
|
const { pi, session, events } = await createSession();
|
||||||
const fakeSession = pi.latestSession();
|
const fakeSession = pi.latestSession();
|
||||||
|
|||||||
@@ -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", () => {
|
test("normalizes Pi MCP proxy calls from requested tool args while running", () => {
|
||||||
const toolCall = parseToolArgs("mcp", {
|
const toolCall = parseToolArgs("mcp", {
|
||||||
tool: "paseo_list_models",
|
tool: "paseo_list_models",
|
||||||
|
|||||||
@@ -340,6 +340,10 @@ export function resolveToolCallName(toolCall: PiTrackedToolCall, result?: PiTool
|
|||||||
export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult): ToolCallDetail {
|
export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult): ToolCallDetail {
|
||||||
const parsedResult = result ?? null;
|
const parsedResult = result ?? null;
|
||||||
|
|
||||||
|
if (isTaskToolCall(toolCall)) {
|
||||||
|
return mapTaskToolDetail(toolCall.args, parsedResult);
|
||||||
|
}
|
||||||
|
|
||||||
switch (toolCall.kind) {
|
switch (toolCall.kind) {
|
||||||
case "bash": {
|
case "bash": {
|
||||||
const summary = resolveToolCallOutput(parsedResult);
|
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 {
|
function mapWriteToolDetail(args: WriteToolInput, result: PiToolResult): ToolCallDetail {
|
||||||
if (result && typeof result !== "string" && result.details && "xdev" in result.details) {
|
if (result && typeof result !== "string" && result.details && "xdev" in result.details) {
|
||||||
const xdev = XdevExecuteDetailsSchema.safeParse(result.details.xdev);
|
const xdev = XdevExecuteDetailsSchema.safeParse(result.details.xdev);
|
||||||
|
|||||||
Reference in New Issue
Block a user