Skip to content

Commit 2a5c346

Browse files
Fix Codex collaboration lifecycle handling
Preserve terminal tool-call status, discard child output before buffering, and reserve the subagent UI for actual spawn operations. Add regressions for structured output, command buffering, active labels, and orchestration rendering. Generated-By: PostHog Code Task-Id: 971044c5-e622-4f07-b1b5-dca9b06d040c
1 parent e284656 commit 2a5c346

9 files changed

Lines changed: 129 additions & 42 deletions

File tree

packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,29 @@ describe("CodexAppServerAgent", () => {
141141
"turn/start": { turn: { id: "turn_1", status: "inProgress" } },
142142
});
143143
const { client, sessionUpdates, extNotifications } = makeFakeClient();
144+
const structuredOutputs: Array<Record<string, unknown>> = [];
144145
const agent = new CodexAppServerAgent(client, {
145146
processOptions: { binaryPath: "/bundle/codex" },
146147
model: "gpt-5.5",
147148
rpcFactory: stub.factory,
149+
onStructuredOutput: async (output) => {
150+
structuredOutputs.push(output);
151+
},
148152
});
153+
const schema = {
154+
type: "object",
155+
properties: { source: { type: "string" } },
156+
required: ["source"],
157+
};
149158

150159
await agent.initialize(init);
151160
await agent.newSession({
152161
cwd: "/repo",
153-
_meta: { environment: "cloud", taskRunId: "run_1" },
162+
_meta: {
163+
environment: "cloud",
164+
jsonSchema: schema,
165+
taskRunId: "run_1",
166+
},
154167
} as unknown as NewSessionRequest);
155168
const promptDone = agent.prompt({
156169
sessionId: "thr_1",
@@ -178,6 +191,27 @@ describe("CodexAppServerAgent", () => {
178191
itemId: "subagent_message_1",
179192
delta: "subagent prose",
180193
});
194+
stub.emit("item/reasoning/textDelta", {
195+
threadId: "subagent_1",
196+
turnId: "subagent_turn_1",
197+
itemId: "subagent_reasoning_1",
198+
delta: "subagent reasoning",
199+
});
200+
stub.emit("item/completed", {
201+
threadId: "subagent_1",
202+
turnId: "subagent_turn_1",
203+
item: {
204+
type: "agentMessage",
205+
id: "subagent_message_1",
206+
text: '{"source":"child"}',
207+
},
208+
});
209+
stub.emit("item/commandExecution/outputDelta", {
210+
threadId: "subagent_1",
211+
turnId: "subagent_turn_1",
212+
itemId: "shared_command_id",
213+
delta: "child command output",
214+
});
181215
stub.emit("thread/tokenUsage/updated", {
182216
threadId: "subagent_1",
183217
tokenUsage: {
@@ -221,6 +255,26 @@ describe("CodexAppServerAgent", () => {
221255
itemId: "message_1",
222256
delta: "parent response",
223257
});
258+
stub.emit("item/completed", {
259+
threadId: "thr_1",
260+
turnId: "turn_1",
261+
item: {
262+
type: "commandExecution",
263+
id: "shared_command_id",
264+
command: "echo parent",
265+
status: "completed",
266+
aggregatedOutput: null,
267+
},
268+
});
269+
stub.emit("item/completed", {
270+
threadId: "thr_1",
271+
turnId: "turn_1",
272+
item: {
273+
type: "agentMessage",
274+
id: "message_1",
275+
text: '{"source":"parent"}',
276+
},
277+
});
224278
stub.emit("turn/completed", {
225279
threadId: "thr_1",
226280
turn: { id: "turn_1", status: "completed" },
@@ -231,6 +285,9 @@ describe("CodexAppServerAgent", () => {
231285
expect(serializedUpdates).toContain("spawn_agent");
232286
expect(serializedUpdates).toContain("parent response");
233287
expect(serializedUpdates).not.toContain("subagent prose");
288+
expect(serializedUpdates).not.toContain("subagent reasoning");
289+
expect(serializedUpdates).not.toContain("child command output");
290+
expect(structuredOutputs).toEqual([{ source: "parent" }]);
234291
expect(
235292
extNotifications.filter(
236293
(notification) => notification.method === "_posthog/turn_complete",

packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,10 +937,12 @@ export class CodexAppServerAgent extends BaseAcpAgent {
937937
}
938938

939939
private handleNotification(method: string, params: unknown): void {
940-
const mappedParams = this.withBufferedCommandOutput(method, params);
941-
const notificationThreadId = readNotificationThreadId(mappedParams);
940+
const notificationThreadId = readNotificationThreadId(params);
942941
const isMainThread =
943942
!notificationThreadId || notificationThreadId === this.threadId;
943+
const mappedParams = isMainThread
944+
? this.withBufferedCommandOutput(method, params)
945+
: params;
944946

945947
if (this.sessionId && !this.session.cancelled) {
946948
if (isMainThread) {

packages/agent/src/adapters/codex-app-server/mapping.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ describe("mapAppServerNotification", () => {
268268
});
269269
});
270270

271-
it("keeps a completed spawn tool call active while its subagent is running", () => {
271+
it("keeps a completed spawn tool call terminal while its subagent is running", () => {
272272
const result = mapAppServerNotification(
273273
"s-1",
274274
APP_SERVER_NOTIFICATIONS.ITEM_COMPLETED,
@@ -293,7 +293,7 @@ describe("mapAppServerNotification", () => {
293293
update: {
294294
sessionUpdate: "tool_call_update",
295295
toolCallId: "spawn-1",
296-
status: "in_progress",
296+
status: "completed",
297297
},
298298
});
299299
});

packages/agent/src/adapters/codex-app-server/mapping.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ function mapItem(
549549
update: {
550550
sessionUpdate: "tool_call_update",
551551
toolCallId: item.id,
552-
status: mapToolStatus(item),
552+
status: mapStatus(item.status),
553553
...(content ? { content } : {}),
554554
},
555555
};
@@ -595,20 +595,6 @@ function mapStatus(
595595
return "in_progress";
596596
}
597597

598-
function mapToolStatus(
599-
item: AppServerItem,
600-
): "completed" | "failed" | "in_progress" {
601-
if (
602-
item.type === "collabAgentToolCall" &&
603-
Object.values(item.agentsStates ?? {}).some(
604-
(agent) => agent?.status === "pendingInit" || agent?.status === "running",
605-
)
606-
) {
607-
return "in_progress";
608-
}
609-
return mapStatus(item.status);
610-
}
611-
612598
function readItem(params: unknown): AppServerItem | null {
613599
if (params && typeof params === "object" && "item" in params) {
614600
const item = (params as Record<string, unknown>).item;
Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
import { ServiceProvider } from "@posthog/di/react";
12
import { posthogToolMeta } from "@posthog/shared";
23
import type { ConversationItem } from "@posthog/ui/features/sessions/components/buildConversationItems";
34
import { Theme } from "@radix-ui/themes";
45
import { render, screen } from "@testing-library/react";
6+
import { Container } from "inversify";
57
import { describe, expect, it } from "vitest";
68
import { ToolGroup } from "./ToolGroup";
79

810
function subagentItem(
911
id: string,
12+
options: {
13+
status?: "completed" | "in_progress";
14+
turnComplete?: boolean;
15+
} = {},
1016
): Extract<ConversationItem, { type: "session_update" }> {
1117
return {
1218
type: "session_update",
@@ -16,26 +22,53 @@ function subagentItem(
1622
toolCallId: id,
1723
title: "Subagent",
1824
kind: "other",
19-
status: "completed",
25+
status: options.status ?? "completed",
2026
_meta: posthogToolMeta({ toolName: "spawn_agent" }),
2127
},
2228
turnContext: {
2329
toolCalls: new Map(),
2430
childItems: new Map(),
2531
turnCancelled: false,
26-
turnComplete: true,
32+
turnComplete: options.turnComplete ?? true,
2733
},
2834
} as Extract<ConversationItem, { type: "session_update" }>;
2935
}
3036

3137
describe("ToolGroup", () => {
3238
it("labels Codex spawn batches as subagents", () => {
3339
render(
34-
<Theme>
35-
<ToolGroup tools={[subagentItem("spawn-1"), subagentItem("spawn-2")]} />
36-
</Theme>,
40+
<ServiceProvider container={new Container()}>
41+
<Theme>
42+
<ToolGroup
43+
tools={[subagentItem("spawn-1"), subagentItem("spawn-2")]}
44+
/>
45+
</Theme>
46+
</ServiceProvider>,
3747
);
3848

3949
expect(screen.getByText("Used Subagents")).toBeInTheDocument();
4050
});
51+
52+
it("labels unresolved Codex spawn batches as active subagents", () => {
53+
render(
54+
<ServiceProvider container={new Container()}>
55+
<Theme>
56+
<ToolGroup
57+
tools={[
58+
subagentItem("spawn-1", {
59+
status: "in_progress",
60+
turnComplete: false,
61+
}),
62+
subagentItem("spawn-2", {
63+
status: "in_progress",
64+
turnComplete: false,
65+
}),
66+
]}
67+
/>
68+
</Theme>
69+
</ServiceProvider>,
70+
);
71+
72+
expect(screen.getByText("Using Subagents")).toBeInTheDocument();
73+
});
4174
});

packages/ui/src/features/sessions/components/new-thread/conversationThreadConfig.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Wrench,
1515
} from "@phosphor-icons/react";
1616
import type { CodeToolKind } from "@posthog/ui/features/sessions/types";
17+
import { SUBAGENT_SPAWN_TOOL_NAMES } from "../session-update/collaborationTools";
1718

1819
/**
1920
* Single source of truth for the modernized conversation thread's tuneable
@@ -57,17 +58,7 @@ export const grouping = {
5758
/**
5859
* Tool names that create a subagent. Counted separately in the chip summary.
5960
*/
60-
subagentToolNames: new Set<string>(["Task", "Agent", "spawn_agent"]),
61-
/** Collaboration tools rendered with the dedicated subagent view. */
62-
collaborationToolNames: new Set<string>([
63-
"Task",
64-
"Agent",
65-
"spawn_agent",
66-
"send_input",
67-
"resume_agent",
68-
"wait_agent",
69-
"close_agent",
70-
]),
61+
subagentToolNames: SUBAGENT_SPAWN_TOOL_NAMES,
7162
/**
7263
* MCP-app tool calls are excluded from collapsing so their iframes stay
7364
* mounted (the `keepMounted` contract). Flip to false to fold them in.

packages/ui/src/features/sessions/components/session-update/ToolCallBlock.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,17 @@ describe("ToolCallBlock codex routing", () => {
115115
expect(screen.getByText("Run tests")).toBeInTheDocument();
116116
expect(screen.getByText("pnpm test")).toBeInTheDocument();
117117
});
118+
119+
it("renders Codex orchestration calls as operations rather than subagents", () => {
120+
renderBlock({
121+
toolCallId: "tc-wait-agent",
122+
title: "Wait for subagents",
123+
kind: "other",
124+
status: "completed",
125+
_meta: posthogToolMeta({ toolName: "wait_agent" }),
126+
});
127+
128+
expect(screen.getByText("Wait for subagents")).toBeInTheDocument();
129+
expect(screen.queryByText(/^Subagent/)).not.toBeInTheDocument();
130+
});
118131
});

packages/ui/src/features/sessions/components/session-update/ToolCallBlock.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { ToolCall } from "@posthog/ui/features/sessions/types";
1616
import { Box } from "@radix-ui/themes";
1717
import type { ConversationItem, TurnContext } from "../buildConversationItems";
1818
import { useChatThreadChrome } from "../chat-thread/chatThreadChrome";
19-
import { grouping } from "../new-thread/conversationThreadConfig";
19+
import { isSubagentSpawnTool } from "./collaborationTools";
2020
import {
2121
MCP_TOOL_BLOCK_COMPONENT,
2222
type McpToolBlockComponent,
@@ -48,7 +48,7 @@ export function ToolCallBlock({
4848

4949
const props = { toolCall, turnCancelled, turnComplete };
5050

51-
if (isSubagentTool(toolName)) {
51+
if (isSubagentSpawnTool(toolName)) {
5252
const subagentChildItems = childItems ?? [];
5353
const turnContext: TurnContext = {
5454
toolCalls: buildChildToolCallsMap(subagentChildItems),
@@ -109,10 +109,6 @@ export function ToolCallBlock({
109109
return <Box>{content}</Box>;
110110
}
111111

112-
function isSubagentTool(toolName: string | undefined): boolean {
113-
return grouping.collaborationToolNames.has(toolName ?? "");
114-
}
115-
116112
function buildChildToolCallsMap(
117113
childItems: ConversationItem[],
118114
): Map<string, ToolCall> {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const SUBAGENT_SPAWN_TOOL_NAMES = new Set<string>([
2+
"Task",
3+
"Agent",
4+
"spawn_agent",
5+
]);
6+
7+
export function isSubagentSpawnTool(toolName: string | undefined): boolean {
8+
return SUBAGENT_SPAWN_TOOL_NAMES.has(toolName ?? "");
9+
}

0 commit comments

Comments
 (0)