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 () => {
|
test("loads Codex persisted history from the app-server thread", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
const requests: Array<{ method: string; params: unknown }> = [];
|
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", () => {
|
it("maps mcp read_file completion with detail", () => {
|
||||||
const item = mapCodexToolCallFromThreadItem(
|
const item = mapCodexToolCallFromThreadItem(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -544,6 +544,14 @@ function readStatus(value: unknown): string | undefined {
|
|||||||
return typeof value.status === "string" ? value.status : 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(
|
function resolveCollabAgentStatus(
|
||||||
item: z.infer<typeof CodexCollabAgentToolCallItemSchema>,
|
item: z.infer<typeof CodexCollabAgentToolCallItemSchema>,
|
||||||
): ToolCallTimelineItem["status"] {
|
): ToolCallTimelineItem["status"] {
|
||||||
@@ -551,10 +559,15 @@ function resolveCollabAgentStatus(
|
|||||||
return "failed";
|
return "failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parentStatus = normalizeToolCallStatus(item.status, null, null);
|
||||||
|
if (parentStatus === "failed") {
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
|
||||||
const childStatuses = Object.values(item.agentsStates ?? {})
|
const childStatuses = Object.values(item.agentsStates ?? {})
|
||||||
.map(readStatus)
|
.map(readStatus)
|
||||||
.filter((status): status is string => typeof status === "string" && status.trim().length > 0)
|
.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")) {
|
if (childStatuses.some((status) => status === "failed")) {
|
||||||
return "failed";
|
return "failed";
|
||||||
@@ -566,7 +579,7 @@ function resolveCollabAgentStatus(
|
|||||||
return childStatuses.every((status) => status === "completed") ? "completed" : "running";
|
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 {
|
function buildMcpToolName(server: string | undefined, tool: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user