mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
fix(pi): mark turn-limited tintinweb subagents as completed (#6594)
Co-authored-by: Chen-DaYi <12897415+Chen-DaYi@users.noreply.github.com>
This commit is contained in:
+150
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createPiExtensionHost } from "../index.js";
|
||||
import { readSubagentFixture, verifySubagentFixture } from "../subagent-fixture-test.js";
|
||||
import type { PiAgentMessage } from "../../rpc-types.js";
|
||||
|
||||
describe("@tintinweb/pi-subagents adapter", () => {
|
||||
test("exposes the background output file while the child is running", () => {
|
||||
@@ -22,6 +23,27 @@ describe("@tintinweb/pi-subagents adapter", () => {
|
||||
]);
|
||||
expect(mapping?.childSessions).toEqual([{ id: "call-1", file: "/tmp/child.output" }]);
|
||||
});
|
||||
test("marks a foreground child that wrapped up at its turn limit as completed", () => {
|
||||
const mapping = createPiExtensionHost().mapToolCall({
|
||||
callId: "call-1",
|
||||
toolName: "Agent",
|
||||
args: { subagent_type: "Explore", prompt: "Inspect" },
|
||||
status: "completed",
|
||||
result: {
|
||||
details: { agentId: "native-1", status: "steered" },
|
||||
content: [{ type: "text", text: "Wrapped up (turn limit)" }],
|
||||
},
|
||||
});
|
||||
expect(mapping?.subagents).toEqual([
|
||||
expect.objectContaining({ id: "call-1", status: "completed" }),
|
||||
]);
|
||||
expect(mapping?.detail).toEqual({
|
||||
type: "sub_agent",
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect",
|
||||
log: "Wrapped up (turn limit)",
|
||||
});
|
||||
});
|
||||
test("maps captured foreground lifecycle live and on replay", async () => {
|
||||
const events = await verifySubagentFixture(
|
||||
readSubagentFixture(new URL("./fixtures/foreground.json", import.meta.url)),
|
||||
@@ -65,6 +87,134 @@ describe("@tintinweb/pi-subagents adapter", () => {
|
||||
call_277179: "completed",
|
||||
});
|
||||
});
|
||||
test.each(["subagent-notification", "subagent-update"])(
|
||||
"completes turn-limited children in a grouped %s",
|
||||
(customType) => {
|
||||
const host = createPiExtensionHost();
|
||||
for (const id of ["1", "2", "3"]) {
|
||||
host.mapToolCall({
|
||||
callId: `call-${id}`,
|
||||
toolName: "Agent",
|
||||
args: { subagent_type: "Explore", prompt: "Inspect" },
|
||||
status: "completed",
|
||||
result: { details: { agentId: `native-${id}`, status: "background" } },
|
||||
});
|
||||
}
|
||||
const mapping = host.mapCustomMessage({
|
||||
role: "custom",
|
||||
customType,
|
||||
content: "Wrapped up (turn limit)",
|
||||
display: true,
|
||||
timestamp: 0,
|
||||
details: {
|
||||
id: "native-1",
|
||||
status: "steered",
|
||||
others: [
|
||||
{ id: "native-2", status: "steered" },
|
||||
{ id: "native-3", status: "running" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(mapping?.subagents).toEqual([
|
||||
expect.objectContaining({ id: "call-1", status: "completed" }),
|
||||
expect.objectContaining({ id: "call-2", status: "completed" }),
|
||||
expect.objectContaining({ id: "call-3", status: "running" }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
test.each(["get_subagent_result", "steer_subagent"])(
|
||||
"completes a turn-limited child when collected through %s",
|
||||
(toolName) => {
|
||||
const host = createPiExtensionHost();
|
||||
host.mapToolCall({
|
||||
callId: "call-1",
|
||||
toolName: "Agent",
|
||||
args: { subagent_type: "Explore", prompt: "Inspect" },
|
||||
status: "completed",
|
||||
result: { details: { agentId: "native-1", status: "background" } },
|
||||
});
|
||||
const mapping = host.mapToolCall({
|
||||
callId: "followup-1",
|
||||
toolName,
|
||||
args: { agent_id: "native-1" },
|
||||
status: "completed",
|
||||
result: {
|
||||
details: { agentId: "native-1", status: "steered" },
|
||||
content: [{ type: "text", text: "Wrapped up (turn limit)" }],
|
||||
},
|
||||
});
|
||||
expect(mapping?.subagents).toEqual([{ type: "upsert", id: "call-1", status: "completed" }]);
|
||||
expect(mapping?.detail).toEqual({
|
||||
type: "sub_agent",
|
||||
description: undefined,
|
||||
log: "Wrapped up (turn limit)",
|
||||
});
|
||||
},
|
||||
);
|
||||
test.each([
|
||||
["background", "running"],
|
||||
["queued", "running"],
|
||||
["running", "running"],
|
||||
["completed", "completed"],
|
||||
["error", "failed"],
|
||||
["aborted", "canceled"],
|
||||
["stopped", "canceled"],
|
||||
["unknown", "running"],
|
||||
])("preserves the mapping of %s to %s", (status, expected) => {
|
||||
const mapping = createPiExtensionHost().mapToolCall({
|
||||
callId: "call-1",
|
||||
toolName: "Agent",
|
||||
args: { subagent_type: "Explore", prompt: "Inspect" },
|
||||
status: "completed",
|
||||
result: { details: { agentId: "native-1", status } },
|
||||
});
|
||||
expect(mapping?.subagents).toEqual([
|
||||
expect.objectContaining({ id: "call-1", status: expected }),
|
||||
]);
|
||||
});
|
||||
test("completes a turn-limited background child live and on history replay", async () => {
|
||||
const path = new URL("./fixtures/background.json", import.meta.url);
|
||||
const source = readSubagentFixture(path);
|
||||
const notification = source.messages.find((message) => message.role === "custom");
|
||||
const notificationDetails = notification?.details;
|
||||
if (
|
||||
!notificationDetails ||
|
||||
typeof notificationDetails !== "object" ||
|
||||
!("outputFile" in notificationDetails) ||
|
||||
typeof notificationDetails.outputFile !== "string"
|
||||
) {
|
||||
throw new Error("Background fixture is missing its child output file");
|
||||
}
|
||||
const fixture = readSubagentFixture(path, {
|
||||
from: notificationDetails.outputFile,
|
||||
to: fileURLToPath(new URL("./fixtures/child-session.jsonl", import.meta.url)),
|
||||
});
|
||||
function wrapUp(message: PiAgentMessage): PiAgentMessage {
|
||||
if (message.role !== "custom") return message;
|
||||
const details = message.details;
|
||||
if (!details || typeof details !== "object") {
|
||||
throw new Error("Background fixture is missing its notification details");
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content: "Wrapped up (turn limit)",
|
||||
details: { ...details, status: "steered" },
|
||||
};
|
||||
}
|
||||
const events = await verifySubagentFixture({
|
||||
...fixture,
|
||||
events: fixture.events.map((event) =>
|
||||
event.type === "message_end" ? { ...event, message: wrapUp(event.message) } : event,
|
||||
),
|
||||
messages: fixture.messages.map(wrapUp),
|
||||
});
|
||||
expect(
|
||||
events
|
||||
.filter((event) => event.event.type === "upsert")
|
||||
.map((event) => (event.event.type === "upsert" ? event.event.status : null)),
|
||||
).toEqual(["running", "running", "completed"]);
|
||||
expect(events.filter((event) => event.event.type === "timeline").length).toBeGreaterThan(0);
|
||||
});
|
||||
test("declines foreign Agent results", () => {
|
||||
expect(
|
||||
createPiExtensionHost().mapToolCall({
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@ const Notification = NotificationEntry.extend({
|
||||
others: z.array(NotificationEntry).optional(),
|
||||
});
|
||||
const status = (value: string): "running" | "completed" | "failed" | "canceled" => {
|
||||
if (value === "completed") return "completed";
|
||||
// A soft turn-limit wrap-up has finished; it is not an in-flight steering request.
|
||||
if (value === "completed" || value === "steered") return "completed";
|
||||
if (value === "error") return "failed";
|
||||
if (value === "aborted" || value === "stopped") return "canceled";
|
||||
return "running";
|
||||
|
||||
Reference in New Issue
Block a user