mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(subagents): separate hidden rows from retained history
This commit is contained in:
@@ -6,7 +6,11 @@ const PARENT_ID = "parent-1";
|
||||
const SUBAGENT_ID = "child-1";
|
||||
|
||||
afterEach(() => {
|
||||
useProviderSubagentStore.setState({ descriptors: new Map(), timelines: new Map() });
|
||||
useProviderSubagentStore.setState({
|
||||
descriptors: new Map(),
|
||||
timelines: new Map(),
|
||||
hiddenFromTrack: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider subagent client store", () => {
|
||||
@@ -165,7 +169,8 @@ describe("provider subagent client store", () => {
|
||||
|
||||
const state = useProviderSubagentStore.getState();
|
||||
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
|
||||
expect(state.descriptors.has(key)).toBe(false);
|
||||
expect(state.descriptors.get(key)?.title).toBe("Finished child");
|
||||
expect(state.hiddenFromTrack.has(key)).toBe(true);
|
||||
expect(state.timelines.get(key)?.tail).toEqual([
|
||||
expect.objectContaining({ kind: "assistant_message", text: "Finished output." }),
|
||||
]);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface ProviderSubagentTimelineState {
|
||||
interface ProviderSubagentState {
|
||||
descriptors: Map<string, ProviderSubagentDescriptorPayload>;
|
||||
timelines: Map<string, ProviderSubagentTimelineState>;
|
||||
hiddenFromTrack: Set<string>;
|
||||
replaceList(
|
||||
serverId: string,
|
||||
parentAgentId: string,
|
||||
@@ -193,14 +194,20 @@ function buildTimelineResponseRows(
|
||||
export const useProviderSubagentStore = create<ProviderSubagentState>((set) => ({
|
||||
descriptors: new Map(),
|
||||
timelines: new Map(),
|
||||
hiddenFromTrack: new Set(),
|
||||
replaceList(serverId, parentAgentId, subagents) {
|
||||
set((state) => {
|
||||
const prefix = parentPrefix(serverId, parentAgentId);
|
||||
const descriptors = new Map(
|
||||
[...state.descriptors].filter(([key]) => !key.startsWith(prefix)),
|
||||
[...state.descriptors].filter(
|
||||
([key]) => !key.startsWith(prefix) || state.hiddenFromTrack.has(key),
|
||||
),
|
||||
);
|
||||
const hiddenFromTrack = new Set(state.hiddenFromTrack);
|
||||
for (const subagent of subagents) {
|
||||
descriptors.set(providerSubagentKey(serverId, parentAgentId, subagent.id), subagent);
|
||||
const key = providerSubagentKey(serverId, parentAgentId, subagent.id);
|
||||
descriptors.set(key, subagent);
|
||||
hiddenFromTrack.delete(key);
|
||||
}
|
||||
const retainedKeys = new Set(descriptors.keys());
|
||||
const timelines = new Map(
|
||||
@@ -217,7 +224,7 @@ export const useProviderSubagentStore = create<ProviderSubagentState>((set) => (
|
||||
);
|
||||
}
|
||||
}
|
||||
return { descriptors, timelines };
|
||||
return { descriptors, timelines, hiddenFromTrack };
|
||||
});
|
||||
},
|
||||
applyUpdate(serverId, payload) {
|
||||
@@ -229,8 +236,10 @@ export const useProviderSubagentStore = create<ProviderSubagentState>((set) => (
|
||||
payload.subagent.id,
|
||||
);
|
||||
const descriptors = new Map(state.descriptors);
|
||||
const hiddenFromTrack = new Set(state.hiddenFromTrack);
|
||||
const previous = descriptors.get(key);
|
||||
descriptors.set(key, payload.subagent);
|
||||
hiddenFromTrack.delete(key);
|
||||
let timelines = state.timelines;
|
||||
const current = state.timelines.get(key);
|
||||
if (current && previous?.status !== payload.subagent.status) {
|
||||
@@ -240,18 +249,22 @@ export const useProviderSubagentStore = create<ProviderSubagentState>((set) => (
|
||||
buildTimelineState(current.rows, current.epoch, payload.subagent, current.hasOlder),
|
||||
);
|
||||
}
|
||||
return { descriptors, timelines };
|
||||
return { descriptors, timelines, hiddenFromTrack };
|
||||
}
|
||||
if (payload.kind === "remove") {
|
||||
const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId);
|
||||
const descriptors = new Map(state.descriptors);
|
||||
descriptors.delete(key);
|
||||
if (payload.retainTimeline) {
|
||||
return { descriptors, timelines: state.timelines };
|
||||
const hiddenFromTrack = new Set(state.hiddenFromTrack);
|
||||
hiddenFromTrack.add(key);
|
||||
return { descriptors, timelines: state.timelines, hiddenFromTrack };
|
||||
}
|
||||
descriptors.delete(key);
|
||||
const timelines = new Map(state.timelines);
|
||||
const hiddenFromTrack = new Set(state.hiddenFromTrack);
|
||||
hiddenFromTrack.delete(key);
|
||||
timelines.delete(key);
|
||||
return { descriptors, timelines };
|
||||
return { descriptors, timelines, hiddenFromTrack };
|
||||
}
|
||||
const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId);
|
||||
const existing = state.timelines.get(key);
|
||||
|
||||
@@ -59,7 +59,11 @@ function setAgents(agents: Agent[]): void {
|
||||
|
||||
afterEach(() => {
|
||||
useSessionStore.getState().clearSession(SERVER_ID);
|
||||
useProviderSubagentStore.setState({ descriptors: new Map(), timelines: new Map() });
|
||||
useProviderSubagentStore.setState({
|
||||
descriptors: new Map(),
|
||||
timelines: new Map(),
|
||||
hiddenFromTrack: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectSubagentsForParent", () => {
|
||||
@@ -90,6 +94,39 @@ describe("selectSubagentsForParent", () => {
|
||||
).toEqual(["provider-child"]);
|
||||
});
|
||||
|
||||
it("hides dismissed provider children from the track while retaining their descriptor", () => {
|
||||
const store = useProviderSubagentStore.getState();
|
||||
store.applyUpdate(SERVER_ID, {
|
||||
kind: "upsert",
|
||||
subagent: {
|
||||
id: "provider-child",
|
||||
parentAgentId: "parent-a",
|
||||
provider: "codex",
|
||||
title: "Provider child",
|
||||
description: null,
|
||||
status: "completed",
|
||||
createdAt: "2026-03-08T10:01:00.000Z",
|
||||
updatedAt: "2026-03-08T10:02:00.000Z",
|
||||
toolCallId: "call-1",
|
||||
},
|
||||
});
|
||||
store.applyUpdate(SERVER_ID, {
|
||||
kind: "remove",
|
||||
parentAgentId: "parent-a",
|
||||
subagentId: "provider-child",
|
||||
retainTimeline: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
selectProviderSubagentsForParent(
|
||||
useProviderSubagentStore.getState(),
|
||||
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
|
||||
true,
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(useProviderSubagentStore.getState().descriptors.size).toBe(1);
|
||||
});
|
||||
|
||||
it("returns only non-archived children for the requested parent", () => {
|
||||
setAgents([
|
||||
makeAgent({ id: "parent-a" }),
|
||||
|
||||
@@ -91,7 +91,7 @@ export function selectProviderSubagentsForParent(
|
||||
const rows: ProviderSubagentRow[] = [];
|
||||
const prefix = `${params.serverId}\0${params.parentAgentId}\0`;
|
||||
for (const [key, subagent] of state.descriptors) {
|
||||
if (!key.startsWith(prefix)) continue;
|
||||
if (!key.startsWith(prefix) || state.hiddenFromTrack.has(key)) continue;
|
||||
rows.push({
|
||||
kind: "provider",
|
||||
id: subagent.id,
|
||||
|
||||
@@ -2857,6 +2857,15 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro
|
||||
this.sessions.push(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
override async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
config?: Partial<AgentSessionConfig>,
|
||||
): Promise<AgentSession> {
|
||||
const session = new ParentSession({ provider: "codex", cwd: config?.cwd ?? workdir });
|
||||
this.sessions.push(session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
const client = new SubagentsClient();
|
||||
const manager = new AgentManager({
|
||||
@@ -2951,6 +2960,26 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro
|
||||
status: "completed",
|
||||
});
|
||||
expect(() => manager.fetchProviderSubagentTimeline(parent.id, "native-finished")).not.toThrow();
|
||||
|
||||
const reloadedManager = new AgentManager({
|
||||
clients: { codex: new SubagentsClient() },
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
await reloadedManager.resumeAgentFromPersistence(
|
||||
{ provider: "codex", sessionId: "persisted-parent" },
|
||||
{ cwd: workdir },
|
||||
parent.id,
|
||||
{ dismissedProviderSubagentIds: ["native-finished", "native-failed"] },
|
||||
);
|
||||
await reloadedManager.hydrateTimelineFromProvider(parent.id);
|
||||
expect(reloadedManager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([
|
||||
"native-running",
|
||||
]);
|
||||
expect(reloadedManager.getProviderSubagent(parent.id, "native-finished")).toMatchObject({
|
||||
id: "native-finished",
|
||||
status: "completed",
|
||||
});
|
||||
});
|
||||
|
||||
test("force provider hydration removes children absent from current history", async () => {
|
||||
|
||||
@@ -3165,8 +3165,9 @@ export class AgentManager {
|
||||
try {
|
||||
for await (const event of agent.session.streamHistory()) {
|
||||
if (event.type === "provider_subagent") {
|
||||
const update = this.applyProviderSubagentEvent(agent, event);
|
||||
if (broadcast && update) {
|
||||
const dismissed = agent.dismissedProviderSubagentIds.has(event.event.id);
|
||||
const update = this.applyProviderSubagentEvent(agent, event, { includeDismissed: true });
|
||||
if (broadcast && update && !dismissed) {
|
||||
this.dispatch({ type: "provider_subagent", event: update });
|
||||
}
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user