mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd12b2c133 | ||
|
|
8ff35abdeb |
@@ -488,6 +488,58 @@ async function pluginMessages(session: Awaited<ReturnType<AgentClient["createSes
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("plugin rewind discards child outcomes from the withdrawn branch", async () => {
|
||||||
|
await withPluginRewindSession(
|
||||||
|
{
|
||||||
|
async handleInput(input, emit) {
|
||||||
|
if (input.type !== "session.open" || input.history !== "replay") return false;
|
||||||
|
emit({
|
||||||
|
type: "session.opened",
|
||||||
|
requestId: input.requestId,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
capabilities: CAPABILITIES,
|
||||||
|
restoration: "core",
|
||||||
|
persistence: input.persistence,
|
||||||
|
cwd: input.config.cwd,
|
||||||
|
});
|
||||||
|
emit({
|
||||||
|
type: "session.opened",
|
||||||
|
sessionId: "rewound-child",
|
||||||
|
parentSessionId: input.sessionId,
|
||||||
|
capabilities: [],
|
||||||
|
restoration: "parent",
|
||||||
|
cwd: input.config.cwd,
|
||||||
|
});
|
||||||
|
emit({ type: "session.closed", sessionId: "rewound-child" });
|
||||||
|
emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (session, harness) => {
|
||||||
|
const open = harness.inputs.find((input) => input.type === "session.open")!;
|
||||||
|
harness.emit({
|
||||||
|
type: "session.opened",
|
||||||
|
sessionId: "rewound-child",
|
||||||
|
parentSessionId: open.sessionId,
|
||||||
|
capabilities: [],
|
||||||
|
restoration: "parent",
|
||||||
|
cwd: tmpdir(),
|
||||||
|
});
|
||||||
|
harness.emit({
|
||||||
|
type: "session.turn",
|
||||||
|
sessionId: "rewound-child",
|
||||||
|
turnId: "withdrawn-turn",
|
||||||
|
state: "failed",
|
||||||
|
});
|
||||||
|
await session.revertConversation!({ messageId: "user-B" });
|
||||||
|
const history = await Array.fromAsync(session.streamHistory());
|
||||||
|
expect(history.findLast((event) => event.type === "provider_subagent")).toMatchObject({
|
||||||
|
event: { type: "upsert", id: "rewound-child", status: "completed" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("combined plugin rewind replays the retained native branch", async () => {
|
test("combined plugin rewind replays the retained native branch", async () => {
|
||||||
await withPluginRewindSession({}, async (session) => {
|
await withPluginRewindSession({}, async (session) => {
|
||||||
await session.revertBoth!({ messageId: "user-B" });
|
await session.revertBoth!({ messageId: "user-B" });
|
||||||
@@ -1234,6 +1286,11 @@ describe("PluginAgentClientRegistry", () => {
|
|||||||
expect(manager.listProviderSubagents(agent.id)).toMatchObject([
|
expect(manager.listProviderSubagents(agent.id)).toMatchObject([
|
||||||
{ id: child.id, title: child.title, status: outcome },
|
{ id: child.id, title: child.title, status: outcome },
|
||||||
]);
|
]);
|
||||||
|
harness.emit({ type: "session.closed", sessionId: "reusable-child" });
|
||||||
|
await nextTurn();
|
||||||
|
expect(manager.listProviderSubagents(agent.id)).toMatchObject([
|
||||||
|
{ id: child.id, title: child.title, status: outcome },
|
||||||
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
await Promise.all(manager.listAgents().map((agent) => manager.closeAgent(agent.id)));
|
await Promise.all(manager.listAgents().map((agent) => manager.closeAgent(agent.id)));
|
||||||
await plugins.shutdown();
|
await plugins.shutdown();
|
||||||
@@ -1268,6 +1325,7 @@ describe("PluginAgentClientRegistry", () => {
|
|||||||
turnId: "second-turn",
|
turnId: "second-turn",
|
||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
|
emit({ type: "session.closed", sessionId: "reusable-child" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1294,6 +1352,100 @@ describe("PluginAgentClientRegistry", () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
test("keeps a child's canceled or failed turn when the child closes normally", async () => {
|
||||||
|
const harness = createProviderHarness();
|
||||||
|
const registry = new PluginAgentClientRegistry(createTestLogger());
|
||||||
|
registry.replace([harness.registration]);
|
||||||
|
const session = await registry.clients()[harness.registration.id]!.createSession({
|
||||||
|
provider: harness.registration.id,
|
||||||
|
cwd: "/workspace",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const events: AgentStreamEvent[] = [];
|
||||||
|
session.subscribe((event) => events.push(event));
|
||||||
|
const open = harness.inputs.find((input) => input.type === "session.open")!;
|
||||||
|
const outcomes = [
|
||||||
|
["child-canceled", "canceled"],
|
||||||
|
["child-failed", "failed"],
|
||||||
|
["child-completed", "completed"],
|
||||||
|
] as const;
|
||||||
|
for (const [sessionId, state] of outcomes) {
|
||||||
|
harness.emit({
|
||||||
|
type: "session.opened",
|
||||||
|
sessionId,
|
||||||
|
parentSessionId: open.sessionId,
|
||||||
|
capabilities: [],
|
||||||
|
restoration: "parent",
|
||||||
|
cwd: "/workspace",
|
||||||
|
});
|
||||||
|
harness.emit({ type: "session.turn", sessionId, turnId: "turn", state: "started" });
|
||||||
|
harness.emit({ type: "session.turn", sessionId, turnId: "turn", state });
|
||||||
|
harness.emit({ type: "session.closed", sessionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastStatus = (id: string) =>
|
||||||
|
events
|
||||||
|
.flatMap((event) =>
|
||||||
|
event.type === "provider_subagent" &&
|
||||||
|
event.event.type === "upsert" &&
|
||||||
|
event.event.id === id
|
||||||
|
? [event.event.status]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.at(-1);
|
||||||
|
expect(lastStatus("child-canceled")).toBe("canceled");
|
||||||
|
expect(lastStatus("child-failed")).toBe("failed");
|
||||||
|
expect(lastStatus("child-completed")).toBe("completed");
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
registry.replace([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ turns: [], error: false, expected: "completed" },
|
||||||
|
{ turns: ["canceled", "started"], error: false, expected: "completed" },
|
||||||
|
{ turns: ["failed", "started", "completed"], error: false, expected: "completed" },
|
||||||
|
{ turns: ["canceled"], error: true, expected: "failed" },
|
||||||
|
] as const)(
|
||||||
|
"closes a plugin child using its latest turn: $turns, error=$error",
|
||||||
|
async ({ turns, error, expected }) => {
|
||||||
|
const harness = createProviderHarness();
|
||||||
|
const registry = new PluginAgentClientRegistry(createTestLogger());
|
||||||
|
registry.replace([harness.registration]);
|
||||||
|
try {
|
||||||
|
const session = await registry.clients()[harness.registration.id]!.createSession({
|
||||||
|
provider: harness.registration.id,
|
||||||
|
cwd: tmpdir(),
|
||||||
|
});
|
||||||
|
const events: AgentStreamEvent[] = [];
|
||||||
|
session.subscribe((event) => events.push(event));
|
||||||
|
const open = harness.inputs.find((input) => input.type === "session.open")!;
|
||||||
|
harness.emit({
|
||||||
|
type: "session.opened",
|
||||||
|
sessionId: "close-control",
|
||||||
|
parentSessionId: open.sessionId,
|
||||||
|
capabilities: [],
|
||||||
|
restoration: "parent",
|
||||||
|
cwd: tmpdir(),
|
||||||
|
});
|
||||||
|
for (const state of turns) {
|
||||||
|
harness.emit({ type: "session.turn", sessionId: "close-control", turnId: "turn", state });
|
||||||
|
}
|
||||||
|
harness.emit({
|
||||||
|
type: "session.closed",
|
||||||
|
sessionId: "close-control",
|
||||||
|
...(error ? { error: { message: "Provider failed to close" } } : {}),
|
||||||
|
});
|
||||||
|
expect(events.at(-1)).toMatchObject({
|
||||||
|
type: "provider_subagent",
|
||||||
|
event: { type: "upsert", id: "close-control", status: expected },
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await registry.shutdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("gates persistence operations on negotiated provider capabilities", async () => {
|
test("gates persistence operations on negotiated provider capabilities", async () => {
|
||||||
const harness = createProviderHarness({ capabilities: ["session.persistence"] });
|
const harness = createProviderHarness({ capabilities: ["session.persistence"] });
|
||||||
|
|||||||
@@ -1200,6 +1200,8 @@ class PluginAgentSession implements AgentSession {
|
|||||||
{ session: ProviderRuntimeSession; unsubscribe: () => void }
|
{ session: ProviderRuntimeSession; unsubscribe: () => void }
|
||||||
>();
|
>();
|
||||||
private readonly childSnapshots = new Map<string, Map<string, TimelineTextSnapshot>>();
|
private readonly childSnapshots = new Map<string, Map<string, TimelineTextSnapshot>>();
|
||||||
|
// Outcome of each child's latest finished turn, so a plain close does not report it as completed.
|
||||||
|
private readonly childTurnOutcomes = new Map<string, "completed" | "canceled" | "failed">();
|
||||||
private unsubscribe: (() => void) | null = null;
|
private unsubscribe: (() => void) | null = null;
|
||||||
private currentTurnId: string | null = null;
|
private currentTurnId: string | null = null;
|
||||||
private closed = false;
|
private closed = false;
|
||||||
@@ -1672,6 +1674,11 @@ class PluginAgentSession implements AgentSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === "session.turn") {
|
if (event.type === "session.turn") {
|
||||||
|
if (event.state === "started") {
|
||||||
|
this.childTurnOutcomes.delete(childId);
|
||||||
|
} else {
|
||||||
|
this.childTurnOutcomes.set(childId, event.state);
|
||||||
|
}
|
||||||
this.publish({
|
this.publish({
|
||||||
type: "provider_subagent",
|
type: "provider_subagent",
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
@@ -1690,7 +1697,10 @@ class PluginAgentSession implements AgentSession {
|
|||||||
event: {
|
event: {
|
||||||
type: "upsert",
|
type: "upsert",
|
||||||
id: childId,
|
id: childId,
|
||||||
status: event.type === "session.runtime_failed" || event.error ? "failed" : "completed",
|
status:
|
||||||
|
event.type === "session.runtime_failed" || event.error
|
||||||
|
? "failed"
|
||||||
|
: (this.childTurnOutcomes.get(childId) ?? "completed"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1708,6 +1718,7 @@ class PluginAgentSession implements AgentSession {
|
|||||||
this.history.clear();
|
this.history.clear();
|
||||||
this.timelineSnapshots.clear();
|
this.timelineSnapshots.clear();
|
||||||
this.childSnapshots.clear();
|
this.childSnapshots.clear();
|
||||||
|
this.childTurnOutcomes.clear();
|
||||||
this.revertTokens.clear();
|
this.revertTokens.clear();
|
||||||
this.pendingPermissions.clear();
|
this.pendingPermissions.clear();
|
||||||
this.permissionResponses.clear();
|
this.permissionResponses.clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user