Skip to content
Open
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
118 changes: 90 additions & 28 deletions packages/client-runtime/src/state/subagentRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,51 +122,108 @@ describe("projectedSubagentsToRuntime workflow expansion", () => {
});

describe("deriveWorkflowGroups over a dynamic workflow", () => {
it("groups members under their phases and counts the run", () => {
const groups = groupsOf([
it("groups and orders members without changing the plan, titles, or phase liveness", () => {
const runtime = projectedSubagentsToRuntime([
subagent({
workflow: {
name: "probe-wf",
phases: [
{ index: 10, title: "Empty" },
{ index: 2, title: "Old Beta" },
{ index: 1, title: "Alpha" },
{ index: 2, title: "Beta" },
],
agents: [
member({ state: "completed" }),
member({ index: 3, label: "alpha:three", state: "completed" }),
member({ index: 2, label: "alpha:two", state: "failed" }),
member({ index: 3, label: "beta:one", phaseIndex: 2, phaseTitle: "Beta" }),
member({
index: 4,
label: "beta:two",
phaseIndex: 2,
phaseTitle: "Beta",
state: "queued",
}),
member({ index: 2, label: "alpha:tie", state: "completed" }),
member({ index: 4, label: "cancelled" }),
member({ index: 5, label: "interrupted" }),
member({ index: 3, label: "unphased:three", phaseIndex: undefined }),
member({ index: 1, label: "unphased:null", phaseIndex: undefined }),
member({ index: 0, label: "unphased:zero", phaseIndex: undefined }),
member({ index: 2, label: "beta:queued", phaseIndex: 2, state: "queued" }),
member({ index: 1, label: "idle", phaseIndex: 3, phaseTitle: "Resume" }),
member({ index: 1, label: "waiting", phaseIndex: 4, phaseTitle: "Wait" }),
member({ index: 1, label: "dynamic", phaseIndex: 5, phaseTitle: undefined }),
],
},
}),
]).map((agent) => {
if (agent.title === "unphased:null") return { ...agent, agentIndex: null };
if (agent.title === "cancelled") return { ...agent, status: "cancelled" as const };
if (agent.title === "interrupted") return { ...agent, status: "interrupted" as const };
if (agent.title === "idle") return { ...agent, status: "idle" as const };
if (agent.title === "waiting") return { ...agent, status: "waiting" as const };
return agent;
});
const before = structuredClone(runtime);
for (const agent of runtime) {
for (const phase of agent.phases) Object.freeze(phase);
Object.freeze(agent.phases);
Object.freeze(agent);
}
Object.freeze(runtime);
const groups = deriveWorkflowGroups(runtime);
expect(groups.map((group) => group.workflow.id)).toEqual(["coordinator"]);
const group = groups[0]!;
expect(
group.phases.map((phase) => [
phase.index,
phase.title,
phase.state,
phase.settledCount,
phase.members.map((agent) => agent.title),
]),
).toEqual([
[
1,
"Alpha",
"done",
5,
["alpha:two", "alpha:tie", "alpha:three", "cancelled", "interrupted"],
],
[2, "Beta", "running", 0, ["beta:queued"]],
[3, "Resume", "running", 0, ["idle"]],
[4, "Wait", "running", 0, ["waiting"]],
[5, "Phase 6", "running", 0, ["dynamic"]],
[10, "Empty", "pending", 0, []],
]);
const group = groups[0];
expect(group?.phases.map((phase) => [phase.title, phase.state])).toEqual([
["Alpha", "done"],
["Beta", "running"],
expect(group.unphasedMembers.map((agent) => agent.title)).toEqual([
"unphased:null",
"unphased:zero",
"unphased:three",
]);
expect(group?.phases.map((phase) => phase.settledCount)).toEqual([2, 0]);
expect(group?.phases.flatMap((phase) => phase.members)).toHaveLength(4);
expect(group?.unphasedMembers).toEqual([]);
expect(group.workflow).toBe(runtime[0]);
for (const agent of [
...group.phases.flatMap((phase) => phase.members),
...group.unphasedMembers,
]) {
expect(agent).toBe(runtime.find((input) => input.title === agent.title));
}
expect(runtime).toEqual(before);
});

it.each([false, true])("keeps a reported phase title, missing title first=%s", (missingFirst) => {
const agents = [member({ phaseTitle: "Review" }), member({ index: 2, phaseTitle: undefined })];
const groups = groupsOf([
subagent({ workflow: { phases: [], agents: missingFirst ? agents.toReversed() : agents } }),
]);
expect(groups[0]?.phases[0]?.title).toBe("Review");
it.each([
["Review", undefined, "Review"],
[undefined, "Review", "Review"],
["Old", "Review", "Review"],
[undefined, undefined, "Phase 2"],
["Review", "", ""],
])("keeps reported phase titles %s then %s", (first, second, expected) => {
const agents = [member({ phaseTitle: first }), member({ index: 2, phaseTitle: second })];
const groups = groupsOf([subagent({ workflow: { phases: [], agents } })]);
expect(groups[0]?.phases[0]?.title).toBe(expected);
});

it("keeps unphased members with their workflow and excludes unrelated agents", () => {
const runtime = projectedSubagentsToRuntime([
subagent({ id: "ordinary" }),
subagent({
id: "earlier",
startedAt: at("2026-08-01T09:00:00.000Z"),
workflow: { phases: [], agents: [member({ phaseIndex: undefined })] },
}),
subagent({
id: "__proto__",
workflow: {
Expand All @@ -178,11 +235,16 @@ describe("deriveWorkflowGroups over a dynamic workflow", () => {
subagent({ id: "orphan", workflow: { phases: [], agents: [member()] } }),
]);
const groups = deriveWorkflowGroups(runtime.filter((agent) => agent.id !== "orphan"));
expect(groups.map((group) => group.workflow.id)).toEqual(["__proto__", "constructor"]);
expect(groups[0]?.phases).toEqual([]);
expect(groups[0]?.unphasedMembers.map((agent) => agent.title)).toEqual(["alpha:one"]);
expect(groups.map((group) => group.workflow.id)).toEqual([
"earlier",
"__proto__",
"constructor",
]);
expect(groups[0]?.unphasedMembers.map((agent) => agent.id)).toEqual(["earlier:agent:1"]);
expect(groups[1]?.phases).toEqual([]);
expect(groups[1]?.unphasedMembers).toEqual([]);
expect(groups[1]?.unphasedMembers.map((agent) => agent.id)).toEqual(["__proto__:agent:1"]);
expect(groups[2]?.phases).toEqual([]);
expect(groups[2]?.unphasedMembers).toEqual([]);
});
});

Expand Down
16 changes: 10 additions & 6 deletions packages/client-runtime/src/state/subagentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ export function deriveWorkflowGroups(
// provider only admits a phase once something in it starts. Members seed
// the map; the declared titles then overwrite whatever they guessed.
const phaseTitles = new Map<number, string>();
const membersByPhase = new Map<number | null, RuntimeSubagent[]>();
for (const member of workflowMembers) {
const phaseMembers = membersByPhase.get(member.phaseIndex);
if (phaseMembers === undefined) membersByPhase.set(member.phaseIndex, [member]);
else phaseMembers.push(member);
if (member.phaseIndex === null) continue;
phaseTitles.set(
member.phaseIndex,
Expand All @@ -265,9 +269,9 @@ export function deriveWorkflowGroups(
.sort((a, b) => a.index - b.index);

const phases = knownPhases.map((phase) => {
const phaseMembers = workflowMembers
.filter((member) => member.phaseIndex === phase.index)
.sort((a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0));
const phaseMembers = (membersByPhase.get(phase.index) ?? []).sort(
(a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0),
);
const hasActiveMember = phaseMembers.some(
// Idle members count as active for phase-liveness: a resumable Codex
// member has not finished the phase.
Expand All @@ -287,9 +291,9 @@ export function deriveWorkflowGroups(
};
});

const unphasedMembers = workflowMembers
.filter((member) => member.phaseIndex === null)
.sort((a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0));
const unphasedMembers = (membersByPhase.get(null) ?? []).sort(
(a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0),
);

return { workflow, phases, unphasedMembers };
});
Expand Down
Loading