mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
fix(agents): preserve failure reasons and failed-turn forking
Typed failure activity no longer appears in assistant replies. Attach the response footer to the error cursor and read lastError for failed schedules and child notifications so those consumers retain their behavior.
This commit is contained in:
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
||||
|
||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||
import { AgentManager } from "./agent-manager.js";
|
||||
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
||||
import { AgentStorage } from "./agent-storage.js";
|
||||
import { setupFinishNotification, waitForAgentRunStartWithTimeout } from "./agent-prompt.js";
|
||||
import type { AgentManagerEvent, ManagedAgent } from "./agent-manager.js";
|
||||
@@ -861,3 +862,57 @@ test("a human pasting an agent envelope stays a human message through provider r
|
||||
clientMessageId: "human-submission",
|
||||
});
|
||||
});
|
||||
|
||||
test("errored finish notifications carry the failure rather than the earlier assistant reply", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-finish-error-"));
|
||||
const logger = createTestLogger();
|
||||
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||
await storage.initialize();
|
||||
const prompts: string[] = [];
|
||||
const manager = new AgentManager({
|
||||
logger,
|
||||
registry: storage,
|
||||
clients: createTestAgentClients({
|
||||
onStartTurn(prompt) {
|
||||
prompts.push(typeof prompt === "string" ? prompt : JSON.stringify(prompt));
|
||||
},
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const parent = await manager.createAgent(
|
||||
{ provider: "claude", cwd: workdir, modeId: "bypassPermissions" },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
const child = await manager.createAgent(
|
||||
{ provider: "claude", cwd: workdir, modeId: "bypassPermissions" },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
await manager.runAgent(child.id, "Earlier successful request");
|
||||
const earlierReply = await manager.getLastAssistantMessage(child.id);
|
||||
expect(earlierReply).toBeTruthy();
|
||||
setupFinishNotification({
|
||||
agentManager: manager,
|
||||
agentStorage: storage,
|
||||
childAgentId: child.id,
|
||||
callerAgentId: parent.id,
|
||||
logger,
|
||||
});
|
||||
await expect(manager.runAgent(child.id, "Emit a turn failure")).rejects.toThrow(
|
||||
"Requested fake provider failure",
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
parseAgentMessage(prompts.find((prompt) => prompt.includes("errored.")) ?? "")?.text,
|
||||
).toContain("<agent-error>\nRequested fake provider failure\n</agent-error>"),
|
||||
);
|
||||
expect(
|
||||
parseAgentMessage(prompts.find((prompt) => prompt.includes("errored.")) ?? "")?.text,
|
||||
).not.toContain("<agent-response>");
|
||||
await manager.closeAgent(child.id);
|
||||
await manager.closeAgent(parent.id);
|
||||
} finally {
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -430,6 +430,7 @@ interface FinishNotificationBodyInput {
|
||||
title: string;
|
||||
reason: FinishNotificationReason;
|
||||
lastAssistantMessage: string | null;
|
||||
lastError?: string;
|
||||
permissionRequest?: AgentPermissionRequest;
|
||||
}
|
||||
|
||||
@@ -450,6 +451,9 @@ function formatFinishNotificationBody(params: FinishNotificationBodyInput): stri
|
||||
)}\n</permission-request>`,
|
||||
);
|
||||
}
|
||||
if (params.reason === "errored" && params.lastError) {
|
||||
sections.push(`<agent-error>\n${params.lastError}\n</agent-error>`);
|
||||
}
|
||||
let lastAssistantMessage = params.lastAssistantMessage?.trim();
|
||||
if (lastAssistantMessage) {
|
||||
if (lastAssistantMessage.length > FINISH_NOTIFICATION_MESSAGE_LIMIT) {
|
||||
@@ -517,12 +521,16 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
|
||||
return;
|
||||
}
|
||||
const title = record?.title ?? childAgentId;
|
||||
const lastAssistantMessage = await agentManager.getLastAssistantMessage(childAgentId);
|
||||
const lastAssistantMessage =
|
||||
reason === "errored" ? null : await agentManager.getLastAssistantMessage(childAgentId);
|
||||
const lastError =
|
||||
reason === "errored" ? agentManager.getAgent(childAgentId)?.lastError : undefined;
|
||||
const body = formatFinishNotificationBody({
|
||||
childAgentId,
|
||||
title,
|
||||
reason,
|
||||
lastAssistantMessage,
|
||||
lastError,
|
||||
permissionRequest,
|
||||
});
|
||||
|
||||
|
||||
@@ -1776,6 +1776,51 @@ describe("ScheduleService", () => {
|
||||
expect(storedAgent?.archivedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
test.each(["new-agent", "agent"] as const)(
|
||||
"scheduled %s failures preserve the provider reason",
|
||||
async (targetType) => {
|
||||
const manager = new AgentManager({
|
||||
logger: createTestLogger(),
|
||||
clients: createTestAgentClients(),
|
||||
registry: agentStorage,
|
||||
});
|
||||
const service = createScheduleService({
|
||||
paseoHome: tempDir,
|
||||
logger: createTestLogger(),
|
||||
agentManager: manager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY,
|
||||
now: () => now,
|
||||
});
|
||||
const agent = await manager.createAgent(
|
||||
{ provider: "claude", cwd: tempDir, modeId: "bypassPermissions" },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
await manager.runAgent(agent.id, "Earlier successful request");
|
||||
const earlierReply = await manager.getLastAssistantMessage(agent.id);
|
||||
expect(earlierReply).toBeTruthy();
|
||||
const schedule = await service.create({
|
||||
prompt: "Emit a turn failure",
|
||||
cadence: { type: "every", everyMs: 60_000 },
|
||||
target:
|
||||
targetType === "agent"
|
||||
? { type: "agent", agentId: agent.id }
|
||||
: {
|
||||
type: "new-agent",
|
||||
config: { provider: "claude", cwd: tempDir, archiveOnFinish: false },
|
||||
},
|
||||
maxRuns: 1,
|
||||
});
|
||||
await service.runOnce(schedule.id);
|
||||
expect((await service.inspect(schedule.id)).runs[0]).toMatchObject({
|
||||
status: "failed",
|
||||
error: "Requested fake provider failure",
|
||||
});
|
||||
await manager.closeAgent(agent.id);
|
||||
},
|
||||
);
|
||||
|
||||
test("records prompt-start failures as failed and archives the scheduled agent", async () => {
|
||||
class StartFailureScheduleSession implements AgentSession {
|
||||
readonly provider = "claude";
|
||||
|
||||
@@ -912,6 +912,12 @@ export class ScheduleService {
|
||||
requireSchedule(updatedSchedule, params.scheduleId);
|
||||
}
|
||||
|
||||
private scheduledAgentFailure(agentId: string): Error {
|
||||
return new Error(
|
||||
this.agentManager.getAgent(agentId)?.lastError ?? `Scheduled agent ${agentId} failed`,
|
||||
);
|
||||
}
|
||||
|
||||
private async executeSchedule(
|
||||
schedule: StoredSchedule,
|
||||
runId: string,
|
||||
@@ -945,7 +951,7 @@ export class ScheduleService {
|
||||
throw new Error(`Scheduled agent ${agent.id} is waiting for permission`);
|
||||
}
|
||||
if (waitResult.status === "error") {
|
||||
throw new Error(waitResult.lastMessage ?? `Scheduled agent ${agent.id} failed`);
|
||||
throw this.scheduledAgentFailure(agent.id);
|
||||
}
|
||||
return {
|
||||
agentId: agent.id,
|
||||
@@ -1013,7 +1019,7 @@ export class ScheduleService {
|
||||
throw new Error(`Scheduled agent ${agent.id} is waiting for permission`);
|
||||
}
|
||||
if (waitResult.status === "error") {
|
||||
throw new Error(waitResult.lastMessage ?? `Scheduled agent ${agent.id} failed`);
|
||||
throw this.scheduledAgentFailure(agent.id);
|
||||
}
|
||||
const timelineText = curateAgentActivity(result.timeline);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user