mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
fix(subagents): preserve active descendants and native names
This commit is contained in:
@@ -2849,15 +2849,18 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro
|
||||
}
|
||||
}
|
||||
class SubagentsClient extends TestAgentClient {
|
||||
private sessionsCreated = 0;
|
||||
readonly sessions: TestAgentSession[] = [];
|
||||
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
this.sessionsCreated += 1;
|
||||
return this.sessionsCreated === 1 ? new ParentSession(config) : new TestAgentSession(config);
|
||||
const session =
|
||||
this.sessions.length === 0 ? new ParentSession(config) : new TestAgentSession(config);
|
||||
this.sessions.push(session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
const client = new SubagentsClient();
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new SubagentsClient() },
|
||||
clients: { codex: client },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
@@ -2869,6 +2872,27 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro
|
||||
undefined,
|
||||
{ labels: { [PARENT_AGENT_ID_LABEL]: parent.id }, workspaceId: undefined },
|
||||
);
|
||||
const managedWithRunningDescendant = await manager.createAgent(
|
||||
{ provider: "codex", cwd: workdir },
|
||||
undefined,
|
||||
{ labels: { [PARENT_AGENT_ID_LABEL]: parent.id }, workspaceId: undefined },
|
||||
);
|
||||
const runningGrandchild = await manager.createAgent(
|
||||
{ provider: "codex", cwd: workdir },
|
||||
undefined,
|
||||
{
|
||||
labels: { [PARENT_AGENT_ID_LABEL]: managedWithRunningDescendant.id },
|
||||
workspaceId: undefined,
|
||||
},
|
||||
);
|
||||
client.sessions[3]?.pushEvent({
|
||||
type: "turn_started",
|
||||
provider: "codex",
|
||||
turnId: "running-grandchild-turn",
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.getAgent(runningGrandchild.id)?.lifecycle).toBe("running");
|
||||
});
|
||||
await manager.hydrateTimelineFromProvider(parent.id);
|
||||
|
||||
const result = await manager.archiveFinishedSubagents(parent.id);
|
||||
@@ -2878,6 +2902,8 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro
|
||||
dismissedProviderSubagentIds: ["native-finished", "native-failed"],
|
||||
});
|
||||
expectArchivedAgentRecord(await storage.get(managedFinished.id), "closed");
|
||||
expect((await storage.get(managedWithRunningDescendant.id))?.archivedAt).toBeFalsy();
|
||||
expect(manager.getAgent(runningGrandchild.id)?.lifecycle).toBe("running");
|
||||
expect(manager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([
|
||||
"native-running",
|
||||
]);
|
||||
|
||||
@@ -971,14 +971,35 @@ export class AgentManager {
|
||||
const parent = this.requirePublicAgent(parentAgentId);
|
||||
const registry = this.requireRegistry();
|
||||
const archivedAgentIds: string[] = [];
|
||||
|
||||
for (const record of await registry.list()) {
|
||||
if (record.archivedAt || record.labels[PARENT_AGENT_ID_LABEL] !== parentAgentId) {
|
||||
continue;
|
||||
const records = (await registry.list()).filter((record) => !record.archivedAt);
|
||||
const recordsByParentId = new Map<string, StoredAgentRecord[]>();
|
||||
for (const record of records) {
|
||||
const recordParentId = record.labels[PARENT_AGENT_ID_LABEL];
|
||||
if (!recordParentId) continue;
|
||||
const siblings = recordsByParentId.get(recordParentId) ?? [];
|
||||
siblings.push(record);
|
||||
recordsByParentId.set(recordParentId, siblings);
|
||||
}
|
||||
const hasBusyDescendant = (agentId: string, visited = new Set<string>()): boolean => {
|
||||
if (visited.has(agentId)) return true;
|
||||
visited.add(agentId);
|
||||
for (const child of recordsByParentId.get(agentId) ?? []) {
|
||||
const liveChild = this.agents.get(child.id);
|
||||
if (
|
||||
isAgentBusy(liveChild?.lifecycle ?? child.lastStatus) ||
|
||||
hasBusyDescendant(child.id, visited)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
visited.delete(agentId);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const record of recordsByParentId.get(parentAgentId) ?? []) {
|
||||
const live = this.agents.get(record.id);
|
||||
const status = live?.lifecycle ?? record.lastStatus;
|
||||
if (isAgentBusy(status)) {
|
||||
if (isAgentBusy(status) || hasBusyDescendant(record.id)) {
|
||||
continue;
|
||||
}
|
||||
if (live) {
|
||||
|
||||
@@ -1847,6 +1847,60 @@ describe("Codex app-server provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("updates a registered child with its later native activity name", () => {
|
||||
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-native-name-later",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
prompt: "Inspect the repository.",
|
||||
receiverThreadIds: ["child-native-name-later"],
|
||||
agentsStates: {
|
||||
"child-native-name-later": { status: "pendingInit", message: null },
|
||||
},
|
||||
},
|
||||
});
|
||||
asInternals(session).handleNotification("item/started", {
|
||||
threadId: "test-thread",
|
||||
item: {
|
||||
type: "subAgentActivity",
|
||||
id: "activity-native-name-later",
|
||||
kind: "started",
|
||||
agentThreadId: "child-native-name-later",
|
||||
agentPath: "/root/research/investigator",
|
||||
},
|
||||
});
|
||||
|
||||
expect(events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "provider_subagent",
|
||||
provider: "codex",
|
||||
event: expect.objectContaining({
|
||||
type: "upsert",
|
||||
id: "child-native-name-later",
|
||||
title: "research/investigator",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(events.at(-1)).toMatchObject({
|
||||
type: "timeline",
|
||||
item: {
|
||||
callId: "call-native-name-later",
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "research/investigator",
|
||||
description: "Inspect the repository.",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("renders child MCP image results in the provider subagent timeline", () => {
|
||||
const session = createSession();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
|
||||
@@ -4843,6 +4843,21 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
if (activity.id) {
|
||||
state.activityItemIds.add(activity.id);
|
||||
}
|
||||
const activityToolCall = mapCodexToolCallFromThreadItem(rawItem, {
|
||||
cwd: this.config.cwd ?? null,
|
||||
});
|
||||
if (
|
||||
activityToolCall?.detail.type === "sub_agent" &&
|
||||
state.toolCall.detail.type === "sub_agent"
|
||||
) {
|
||||
state.toolCall = {
|
||||
...state.toolCall,
|
||||
detail: {
|
||||
...state.toolCall.detail,
|
||||
subAgentType: activityToolCall.detail.subAgentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
this.emitSubAgentActivityUpdate(
|
||||
callId,
|
||||
activity.kind === "interrupted" ? "canceled" : "running",
|
||||
|
||||
Reference in New Issue
Block a user