Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 293 additions & 18 deletions plugins/provider-codex/src/translator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,22 @@ describe("codex command output capture across reordering", () => {
describe("codex subagent activity correlation", () => {
const rootProviderThreadId = "root-provider-thread";

function rawCollaborationCall(args: {
callId: string;
name: "followup_task" | "send_message";
}) {
return codexEvent("rawResponseItem/completed", {
threadId: rootProviderThreadId,
turnId: "parent-turn",
item: {
type: "function_call",
name: args.name,
arguments: '{"target":"/root/lifecycle_child"}',
call_id: args.callId,
},
});
}

function subAgentActivity(args: {
agentThreadId?: string;
id: string;
Expand Down Expand Up @@ -900,13 +916,15 @@ describe("codex subagent activity correlation", () => {
);
harness.translate(childTurnCompleted("child-turn-1"));

// A follow-up to a settled agent re-opens its delegation row (same item
// id): the agent works again, and an open delegation is open work.
// `interacted` alone is ambiguous: Codex uses it for both followup_task
// and send_message. Wait for the child turn before reopening the row.
expect(
harness.translate(
subAgentActivity({ id: "interaction-1", kind: "interacted" }),
),
).toEqual([
).toEqual([]);

expect(harness.translate(childTurnStarted("child-turn-2"))).toEqual([
expect.objectContaining({
type: "item/started",
scope: turnScope(harness.turnId("parent-turn")),
Expand All @@ -916,15 +934,12 @@ describe("codex subagent activity correlation", () => {
status: "pending",
}),
}),
]);

expect(harness.translate(childTurnStarted("child-turn-2"))).toContainEqual(
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId("child-turn-2")),
parentToolCallId: harness.itemId("subagent-call-1"),
}),
);
]);

// The resumed turn settles the re-opened delegation again.
const resumedTurnCompleted = harness.translate(
Expand All @@ -947,9 +962,264 @@ describe("codex subagent activity correlation", () => {
]);
});

// Follow-ups queue: two interactions owe two more child turns. The re-arm is
// counted, so terminalizing the agent after the first follow-up must not
// discard the link the second one still needs.
// When app-server supplies the raw collaboration call, it distinguishes a
// turn-producing followup from a message that must not reserve the next
// native turn. Resumed sessions can omit this notification; the rawless
// cases below cover that event shape.
it("links an unknown resumed subagent from the raw followup intent after translator restart", () => {
const harness = createHarness();

expect(
harness.translate(
rawCollaborationCall({
callId: "message-call",
name: "send_message",
}),
),
).toEqual([]);
expect(
harness.translate(
subAgentActivity({ id: "message-call", kind: "interacted" }),
),
).toEqual([]);

expect(
harness.translate(
rawCollaborationCall({
callId: "followup-call",
name: "followup_task",
}),
),
).toEqual([]);
expect(
harness.translate(
subAgentActivity({ id: "followup-call", kind: "interacted" }),
),
).toEqual([
expect.objectContaining({
type: "item/started",
scope: turnScope(harness.turnId("parent-turn")),
item: expect.objectContaining({
type: "delegation",
id: harness.itemId("followup-call"),
childRef: "agent-thread-1",
status: "pending",
}),
}),
]);

expect(
harness.translate(childTurnStarted("resumed-child-turn")),
).toContainEqual(
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId("resumed-child-turn")),
parentToolCallId: harness.itemId("followup-call"),
}),
);
});

it("does not reopen a known terminal subagent for send_message", () => {
const harness = createHarness();
harness.translate(
subAgentActivity({ id: "subagent-call-1", kind: "started" }),
);
harness.translate(childTurnStarted("child-turn-1"));
harness.translate(childTurnCompleted("child-turn-1"));

harness.translate(
rawCollaborationCall({ callId: "message-call", name: "send_message" }),
);
expect(
harness.translate(
subAgentActivity({ id: "message-call", kind: "interacted" }),
),
).toEqual([]);

expect(
harness.translator.prepareTurnStart({
clientRequestId: "creq_after_message",
providerThreadId: rootProviderThreadId,
}),
).not.toBeNull();
const nextRootTurn = harness
.translate(childTurnStarted("next-root-turn"))
.find((event) => event.type === "turn/started");
expect(nextRootTurn).not.toHaveProperty("parentToolCallId");
});

it("links a rawless resumed subagent when its child turn starts", () => {
const harness = createHarness();

expect(
harness.translate(
subAgentActivity({ id: "rawless-followup", kind: "interacted" }),
),
).toEqual([]);

const resumedEvents = harness.translate(
childTurnStarted("rawless-child-turn"),
);
expect(resumedEvents).toEqual([
expect.objectContaining({
type: "item/started",
scope: turnScope(harness.turnId("parent-turn")),
item: expect.objectContaining({
type: "delegation",
id: harness.itemId("rawless-followup"),
childRef: "agent-thread-1",
}),
}),
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId("rawless-child-turn")),
parentToolCallId: harness.itemId("rawless-followup"),
}),
]);
expect(resumedEvents[0]).not.toHaveProperty("parentToolCallId");
expect(resumedEvents[0]).not.toHaveProperty("item.parentToolCallId");
});

// Production resumes report the interaction on the root thread and the
// resulting child turn on the agent's own provider thread. Once the parent
// settles, a new root prompt can start while that child is still running;
// the two provider-thread-scoped correlations must remain independent.
it("keeps root input correlation independent from a rawless resumed child thread", () => {
const harness = createHarness();

expect(
harness.translate(
subAgentActivity({ id: "rawless-followup", kind: "interacted" }),
),
).toEqual([]);

expect(
harness.translate(
childTurnStarted("rawless-child-turn", "agent-thread-1"),
),
).toEqual([
expect.objectContaining({
type: "item/started",
scope: turnScope(harness.turnId("parent-turn")),
item: expect.objectContaining({
type: "delegation",
id: harness.itemId("rawless-followup"),
childRef: "agent-thread-1",
}),
}),
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId("rawless-child-turn")),
parentToolCallId: harness.itemId("rawless-followup"),
}),
]);

harness.translate(childTurnCompleted("parent-turn"));
expect(
harness.translator.prepareTurnStart({
clientRequestId: "creq_while_child_running",
providerThreadId: rootProviderThreadId,
}),
).not.toBeNull();

const rootEvents = harness.translate(childTurnStarted("next-root-turn"));
expect(rootEvents).toContainEqual(
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId("next-root-turn")),
}),
);
expect(rootEvents).toContainEqual(
expect.objectContaining({
type: "turn/input/accepted",
scope: turnScope(harness.turnId("next-root-turn")),
clientRequestId: "creq_while_child_running",
}),
);
expect(
rootEvents.find((event) => event.type === "turn/started"),
).not.toHaveProperty("parentToolCallId");

expect(
harness
.translate(childTurnCompleted("rawless-child-turn", "agent-thread-1"))
.map((event) => event.type),
).toEqual(["turn/completed", "item/completed"]);
});

it("discards a rawless message interaction at its parent boundary", () => {
const harness = createHarness();
expect(
harness.translate(
subAgentActivity({ id: "rawless-message", kind: "interacted" }),
),
).toEqual([]);
harness.translate(childTurnCompleted("parent-turn"));

harness.translator.prepareTurnStart({
clientRequestId: "creq_after_rawless_message",
providerThreadId: rootProviderThreadId,
});
const nextRootTurn = harness
.translate(childTurnStarted("next-root-after-message"))
.find((event) => event.type === "turn/started");
expect(nextRootTurn).not.toHaveProperty("parentToolCallId");
});

// A rawless message is not evidence that the next turn belongs to its
// target. If a different known child starts on the multiplexed root thread,
// that child's explicit pending delegation must win; the message is then
// discarded with its parent and cannot claim a later root turn either.
it("does not attach a rawless message to an unrelated multiplexed child", () => {
const harness = createHarness();
expect(
harness.translate(
subAgentActivity({
agentThreadId: "message-target-thread",
id: "rawless-message",
kind: "interacted",
}),
),
).toEqual([]);

harness.translate(
subAgentActivity({
agentThreadId: "unrelated-agent-thread",
id: "unrelated-subagent-call",
kind: "started",
}),
);
const unrelatedChild = harness
.translate(childTurnStarted("unrelated-child-turn"))
.find((event) => event.type === "turn/started");
expect(unrelatedChild).toEqual(
expect.objectContaining({
type: "turn/started",
parentToolCallId: harness.itemId("unrelated-subagent-call"),
}),
);
expect(unrelatedChild).not.toHaveProperty(
"parentToolCallId",
harness.itemId("rawless-message"),
);

harness.translate(childTurnCompleted("unrelated-child-turn"));
harness.translate(childTurnCompleted("parent-turn"));
expect(
harness.translator.prepareTurnStart({
clientRequestId: "creq_after_unrelated_child",
providerThreadId: rootProviderThreadId,
}),
).not.toBeNull();
const nextRootTurn = harness
.translate(childTurnStarted("next-root-after-unrelated-child"))
.find((event) => event.type === "turn/started");
expect(nextRootTurn).not.toHaveProperty("parentToolCallId");
});

// Each child turn consumes one ambiguous interaction. Settling the first
// resumed turn must not discard the second interaction that still awaits
// its own turn-producing proof.
it("preserves the parent link across queued follow-up resumes", () => {
const harness = createHarness();
harness.translate(
Expand All @@ -958,14 +1228,15 @@ describe("codex subagent activity correlation", () => {
harness.translate(childTurnStarted("child-turn-1"));
harness.translate(childTurnCompleted("child-turn-1"));

// The first follow-up re-opens the delegation; the second finds it open.
// Neither ambiguous interaction re-opens the delegation until a child
// turn proves that it was a turn-producing follow-up.
expect(
harness
.translate(
subAgentActivity({ id: "interaction-1", kind: "interacted" }),
)
.map((event) => event.type),
).toEqual(["item/started"]);
).toEqual([]);
expect(
harness.translate(
subAgentActivity({ id: "interaction-2", kind: "interacted" }),
Expand All @@ -975,21 +1246,25 @@ describe("codex subagent activity correlation", () => {
for (const index of [2, 3]) {
expect(
harness.translate(childTurnStarted(`child-turn-${index}`)),
).toContainEqual(
).toEqual([
expect.objectContaining({
type: "item/started",
item: expect.objectContaining({
type: "delegation",
id: harness.itemId("subagent-call-1"),
}),
}),
expect.objectContaining({
type: "turn/started",
scope: turnScope(harness.turnId(`child-turn-${index}`)),
parentToolCallId: harness.itemId("subagent-call-1"),
}),
);
// The delegation closes only once the last owed follow-up turn settles.
]);
expect(
harness
.translate(childTurnCompleted(`child-turn-${index}`))
.map((event) => event.type),
).toEqual(
index === 3 ? ["turn/completed", "item/completed"] : ["turn/completed"],
);
).toEqual(["turn/completed", "item/completed"]);
}
});

Expand Down
Loading
Loading