fix(server): keep Codex sub-agent running on transient child error state
Fixes #1071
This commit is contained in:
@@ -1352,6 +1352,40 @@ describe("Codex app-server provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("does not synthesize a parent sub-agent failure from child error state alone", () => {
|
||||
const session = createSession();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
session.subscribe((event) => events.push(event));
|
||||
|
||||
asInternals(session).handleNotification("item/completed", {
|
||||
threadId: "test-thread",
|
||||
item: {
|
||||
type: "collabAgentToolCall",
|
||||
id: "call-sub-agent-transient-child-error",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
prompt: "Validate the child agent result.",
|
||||
receiverThreadIds: ["child-thread-1"],
|
||||
agentsStates: {
|
||||
"child-thread-1": { status: "error", message: "Sub-agent failed" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(events.at(-1)?.item).toMatchObject({
|
||||
type: "tool_call",
|
||||
callId: "call-sub-agent-transient-child-error",
|
||||
name: "Sub-agent",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Validate the child agent result.",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("loads Codex persisted history from the app-server thread", async () => {
|
||||
const session = createSession();
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
|
||||
@@ -212,6 +212,64 @@ describe("codex tool-call mapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail a collabAgentToolCall from child error state alone", () => {
|
||||
const item = mapCodexToolCallFromThreadItem({
|
||||
type: "collabAgentToolCall",
|
||||
id: "call-sub-agent-transient-child-error",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
prompt: "Inspect the Codex stream path.",
|
||||
receiverThreadIds: ["child-thread-1"],
|
||||
agentsStates: {
|
||||
"child-thread-1": { status: "error", message: "Sub-agent failed" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(item).toEqual({
|
||||
type: "tool_call",
|
||||
callId: "call-sub-agent-transient-child-error",
|
||||
name: "Sub-agent",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Inspect the Codex stream path.",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("still fails a collabAgentToolCall from an explicitly failed child state", () => {
|
||||
const item = mapCodexToolCallFromThreadItem({
|
||||
type: "collabAgentToolCall",
|
||||
id: "call-sub-agent-child-failed",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
prompt: "Inspect the Codex stream path.",
|
||||
receiverThreadIds: ["child-thread-1"],
|
||||
agentsStates: {
|
||||
"child-thread-1": { status: "failed", message: "Child failed" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(item).toEqual({
|
||||
type: "tool_call",
|
||||
callId: "call-sub-agent-child-failed",
|
||||
name: "Sub-agent",
|
||||
status: "failed",
|
||||
error: { message: "Sub-agent failed" },
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Inspect the Codex stream path.",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps mcp read_file completion with detail", () => {
|
||||
const item = mapCodexToolCallFromThreadItem(
|
||||
{
|
||||
|
||||
@@ -544,6 +544,14 @@ function readStatus(value: unknown): string | undefined {
|
||||
return typeof value.status === "string" ? value.status : undefined;
|
||||
}
|
||||
|
||||
function normalizeCollabAgentChildStatus(status: string): ToolCallTimelineItem["status"] {
|
||||
const normalized = status.trim().toLowerCase();
|
||||
if (normalized === "error" || normalized === "errored") {
|
||||
return "running";
|
||||
}
|
||||
return normalizeToolCallStatus(status, null, null);
|
||||
}
|
||||
|
||||
function resolveCollabAgentStatus(
|
||||
item: z.infer<typeof CodexCollabAgentToolCallItemSchema>,
|
||||
): ToolCallTimelineItem["status"] {
|
||||
@@ -551,10 +559,15 @@ function resolveCollabAgentStatus(
|
||||
return "failed";
|
||||
}
|
||||
|
||||
const parentStatus = normalizeToolCallStatus(item.status, null, null);
|
||||
if (parentStatus === "failed") {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
const childStatuses = Object.values(item.agentsStates ?? {})
|
||||
.map(readStatus)
|
||||
.filter((status): status is string => typeof status === "string" && status.trim().length > 0)
|
||||
.map((status) => normalizeToolCallStatus(status, null, null));
|
||||
.map(normalizeCollabAgentChildStatus);
|
||||
|
||||
if (childStatuses.some((status) => status === "failed")) {
|
||||
return "failed";
|
||||
@@ -566,7 +579,7 @@ function resolveCollabAgentStatus(
|
||||
return childStatuses.every((status) => status === "completed") ? "completed" : "running";
|
||||
}
|
||||
|
||||
return normalizeToolCallStatus(item.status, item.error ?? null, null);
|
||||
return parentStatus;
|
||||
}
|
||||
|
||||
function buildMcpToolName(server: string | undefined, tool: string): string {
|
||||
|
||||
Reference in New Issue
Block a user