fix(agents): retain complete failure outcomes through delivery

Capture the failure reason when the terminal notification is queued, preserve provider diagnostics at their producer, and apply the existing notification text budget. Failed responses also keep their completion timing when their footer is anchored to error activity.
This commit is contained in:
paseo-bot[bot]
2026-10-13 13:20:08 +02:00
parent 6d7aaf5461
commit e1ce819d11
14 changed files with 203 additions and 112 deletions
@@ -82,7 +82,7 @@ function layoutFor(input: { platform: "web" | "native"; isTurnActive: boolean })
isTurnActive: input.isTurnActive,
history: strategy.orderTail([userMessage("user"), assistant]),
liveHead: [],
timingByAssistantId: timingFor(assistant.id),
timingByResponseItemId: timingFor(assistant.id),
}),
};
}
@@ -126,7 +126,7 @@ describe("turn footer spacing", () => {
isTurnActive,
history: strategy.orderTail([userMessage("user"), first]),
liveHead: strategy.orderHead([last]),
timingByAssistantId: timingFor(last.id),
timingByResponseItemId: timingFor(last.id),
});
const firstLayout = layout.history.find((item) => item.item.id === first.id);
const lastLayout = layout.liveHead.find((item) => item.item.id === last.id);
+3 -3
View File
@@ -105,7 +105,7 @@ function layoutFor(input: {
strategy,
streamHead: input.head ?? [],
}),
timingByAssistantId: timingFor(...(input.timingIds ?? [])),
timingByResponseItemId: timingFor(...(input.timingIds ?? [])),
});
}
@@ -201,7 +201,7 @@ describe("layoutStream", () => {
isTurnActive: true,
history,
liveHead,
timingByAssistantId: timingFor(priorAssistant.id),
timingByResponseItemId: timingFor(priorAssistant.id),
});
liveHead[0] = { ...liveHead[0]!, turnId: "turn-2" };
@@ -210,7 +210,7 @@ describe("layoutStream", () => {
isTurnActive: true,
history,
liveHead,
timingByAssistantId: timingFor(priorAssistant.id),
timingByResponseItemId: timingFor(priorAssistant.id),
});
expect(footerOwners(first)).toEqual([]);
+10 -10
View File
@@ -39,13 +39,13 @@ export interface StreamLayoutInput {
isTurnActive: boolean;
history: StreamItem[];
liveHead: StreamItem[];
timingByAssistantId: Map<string, TurnTiming>;
timingByResponseItemId: Map<string, TurnTiming>;
}
interface LayoutSegmentInput {
strategy: StreamStrategy;
items: StreamItem[];
timingByAssistantId: Map<string, TurnTiming>;
timingByResponseItemId: Map<string, TurnTiming>;
auxiliaryTurnFooter: TurnFooterHost | null;
hasAuxiliaryFooter: boolean;
frameOrder: StreamFrameChildOrder;
@@ -67,12 +67,12 @@ function createTurnFooterHost(input: {
item: StreamItem;
items: StreamItem[];
index: number;
timingByAssistantId: Map<string, TurnTiming>;
timingByResponseItemId: Map<string, TurnTiming>;
}): TurnFooterHost {
return {
itemId: input.item.id,
items: input.items,
timing: input.timingByAssistantId.get(input.item.id),
timing: input.timingByResponseItemId.get(input.item.id),
startIndex: input.index,
};
}
@@ -147,7 +147,7 @@ function resolveAuxiliaryTurnFooter(input: StreamLayoutInput): TurnFooterHost |
item: source.item,
items: source.items,
index: source.index,
timingByAssistantId: input.timingByAssistantId,
timingByResponseItemId: input.timingByResponseItemId,
});
}
@@ -157,7 +157,7 @@ function resolveCompletedFooter(input: {
index: number;
item: StreamItem;
belowItem: StreamItem | null;
timingByAssistantId: Map<string, TurnTiming>;
timingByResponseItemId: Map<string, TurnTiming>;
auxiliaryTurnFooter: TurnFooterHost | null;
boundaryAboveItems: StreamItem[] | null;
boundaryAboveIndex: number | null;
@@ -180,7 +180,7 @@ function resolveCompletedFooter(input: {
item: source.item,
items: source.items,
index: source.index,
timingByAssistantId: input.timingByAssistantId,
timingByResponseItemId: input.timingByResponseItemId,
});
}
@@ -309,7 +309,7 @@ function layoutSegmentItem(
index,
item,
belowItem,
timingByAssistantId: input.timingByAssistantId,
timingByResponseItemId: input.timingByResponseItemId,
auxiliaryTurnFooter: input.auxiliaryTurnFooter,
boundaryAboveItems: input.boundaryAboveItems,
boundaryAboveIndex: input.boundaryAboveIndex,
@@ -380,7 +380,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout {
history = layoutSegment({
strategy: input.strategy,
items: input.history,
timingByAssistantId: input.timingByAssistantId,
timingByResponseItemId: input.timingByResponseItemId,
auxiliaryTurnFooter,
hasAuxiliaryFooter,
frameOrder,
@@ -400,7 +400,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout {
const liveHead = layoutSegment({
strategy: input.strategy,
items: input.liveHead,
timingByAssistantId: input.timingByAssistantId,
timingByResponseItemId: input.timingByResponseItemId,
auxiliaryTurnFooter,
hasAuxiliaryFooter,
frameOrder,
+6 -6
View File
@@ -69,8 +69,8 @@ describe("buildAgentStreamRenderModel", () => {
historyStart: 2,
});
expect(model.turnTiming.byAssistantId.has("hidden-a")).toBe(false);
expect(model.turnTiming.byAssistantId.get("visible-a")).toEqual({
expect(model.turnTiming.byResponseItemId.has("hidden-a")).toBe(false);
expect(model.turnTiming.byResponseItemId.get("visible-a")).toEqual({
completedAt: tail[3]?.timestamp,
durationMs: 1000,
});
@@ -184,7 +184,7 @@ describe("buildAgentStreamRenderModel", () => {
});
expect(model.turnTiming.runningStartedAt).toBe(tail[0]?.timestamp);
expect(model.turnTiming.byAssistantId.has("live-a")).toBe(false);
expect(model.turnTiming.byResponseItemId.has("live-a")).toBe(false);
});
it("maps completed turn timing to assistant ids across committed history and live head", () => {
@@ -201,7 +201,7 @@ describe("buildAgentStreamRenderModel", () => {
});
expect(model.turnTiming.runningStartedAt).toBe(null);
expect(model.turnTiming.byAssistantId.get("live-a")).toEqual({
expect(model.turnTiming.byResponseItemId.get("live-a")).toEqual({
completedAt: head[0]?.timestamp,
durationMs: 3000,
});
@@ -220,7 +220,7 @@ describe("buildAgentStreamRenderModel", () => {
});
expect(model.segments.historyMounted.map((item) => item.id)).toEqual(["a1", "u1"]);
expect(model.turnTiming.byAssistantId.get("a1")).toEqual({
expect(model.turnTiming.byResponseItemId.get("a1")).toEqual({
completedAt: tail[1]?.timestamp,
durationMs: 3000,
});
@@ -238,6 +238,6 @@ describe("buildAgentStreamRenderModel", () => {
isMobileBreakpoint: false,
});
expect(model.turnTiming.byAssistantId.size).toBe(0);
expect(model.turnTiming.byResponseItemId.size).toBe(0);
});
});
@@ -53,7 +53,7 @@ function layoutFor(items: StreamItem[], isTurnActive: boolean) {
isTurnActive,
history: model.segments.historyMounted,
liveHead: model.segments.liveHead,
timingByAssistantId: model.turnTiming.byAssistantId,
timingByResponseItemId: model.turnTiming.byResponseItemId,
}),
};
}
@@ -123,7 +123,7 @@ describe("canonical turn membership", () => {
const completedItems = [...build(turnId), assistant("done", 9, turnId)];
const completed = layoutFor(completedItems, false);
expect(completedFooterIds(completed.layout)).toEqual(["done"]);
expect(completed.model.turnTiming.byAssistantId.get("done")).toMatchObject({
expect(completed.model.turnTiming.byResponseItemId.get("done")).toMatchObject({
completedAt: at(9),
durationMs: 8000,
});
+2 -2
View File
@@ -593,12 +593,12 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
isTurnActive,
history: baseRenderModel.history,
liveHead: baseRenderModel.segments.liveHead,
timingByAssistantId: baseRenderModel.turnTiming.byAssistantId,
timingByResponseItemId: baseRenderModel.turnTiming.byResponseItemId,
}),
[
baseRenderModel.history,
baseRenderModel.segments.liveHead,
baseRenderModel.turnTiming.byAssistantId,
baseRenderModel.turnTiming.byResponseItemId,
isTurnActive,
streamRenderStrategy,
],
+30 -5
View File
@@ -22,6 +22,31 @@ function assistant(id: string, timestamp: Date): StreamItem {
}
describe("deriveStreamTurnTiming", () => {
it("retains completed timing for a failed response with or without assistant text", () => {
const userAt = new Date("2026-05-15T00:00:00.000Z");
const failedAt = new Date("2026-05-15T00:00:07.000Z");
const error: StreamItem = {
kind: "notification",
sourceType: "error",
level: "error",
id: "error-1",
message: "Provider failed",
timestamp: failedAt,
};
for (const replies of [[], [assistant("assistant-1", new Date("2026-05-15T00:00:03.000Z"))]]) {
const timing = deriveStreamTurnTiming({
isTurnActive: false,
activeTurnStartedAt: null,
tail: [user("user-1", userAt), ...replies, error],
head: [],
});
assert.deepEqual(timing.byResponseItemId.get(error.id), {
completedAt: failedAt,
durationMs: 7000,
});
}
});
it("starts elapsed time from the submitted prompt", () => {
const submittedAt = new Date("2026-05-15T00:00:00.000Z");
@@ -51,7 +76,7 @@ describe("deriveStreamTurnTiming", () => {
});
assert.equal(timing.runningStartedAt, secondUserAt);
assert.equal(timing.byAssistantId.has("a2"), false);
assert.equal(timing.byResponseItemId.has("a2"), false);
});
it("derives completed turn timing from user and assistant item timestamps", () => {
@@ -69,7 +94,7 @@ describe("deriveStreamTurnTiming", () => {
head: [],
});
assert.deepEqual(timing.byAssistantId.get("a1"), {
assert.deepEqual(timing.byResponseItemId.get("a1"), {
completedAt: assistantAt,
durationMs: 7000,
});
@@ -95,8 +120,8 @@ describe("deriveStreamTurnTiming", () => {
completedAt: lastAssistantAt,
durationMs: 7000,
};
assert.deepEqual(timing.byAssistantId.get("a1"), expected);
assert.deepEqual(timing.byAssistantId.get("a2"), expected);
assert.deepEqual(timing.byResponseItemId.get("a1"), expected);
assert.deepEqual(timing.byResponseItemId.get("a2"), expected);
});
it("preserves the completion timestamp when a canonical turn has no visible prompt", () => {
@@ -120,7 +145,7 @@ describe("deriveStreamTurnTiming", () => {
head: [],
});
assert.deepEqual(timing.byAssistantId.get("hidden-prompt-a2"), {
assert.deepEqual(timing.byResponseItemId.get("hidden-prompt-a2"), {
completedAt: hiddenPromptTurnAt,
durationMs: null,
});
+13 -10
View File
@@ -7,7 +7,7 @@ export interface TurnTiming {
}
export interface StreamTurnTiming {
byAssistantId: Map<string, TurnTiming>;
byResponseItemId: Map<string, TurnTiming>;
runningStartedAt: Date | null;
}
@@ -17,14 +17,14 @@ export function deriveStreamTurnTiming(params: {
tail: StreamItem[];
head: StreamItem[];
}): StreamTurnTiming {
const byAssistantId = new Map<string, TurnTiming>();
const byResponseItemId = new Map<string, TurnTiming>();
let currentUserAt: Date | null = null;
let currentLastItemAt: Date | null = null;
let currentAssistantIds: string[] = [];
let currentResponseItemIds: string[] = [];
let previousItem: StreamItem | null = null;
const flushCompletedTurn = () => {
if (!currentLastItemAt || currentAssistantIds.length === 0) {
if (!currentLastItemAt || currentResponseItemIds.length === 0) {
return;
}
const timing: TurnTiming = {
@@ -33,8 +33,8 @@ export function deriveStreamTurnTiming(params: {
? Math.max(0, currentLastItemAt.getTime() - currentUserAt.getTime())
: null,
};
for (const id of currentAssistantIds) {
byAssistantId.set(id, timing);
for (const id of currentResponseItemIds) {
byResponseItemId.set(id, timing);
}
};
@@ -43,11 +43,14 @@ export function deriveStreamTurnTiming(params: {
flushCompletedTurn();
currentUserAt = item.kind === "user_message" ? item.timestamp : null;
currentLastItemAt = null;
currentAssistantIds = [];
currentResponseItemIds = [];
}
currentLastItemAt = item.timestamp;
if (item.kind === "assistant_message") {
currentAssistantIds.push(item.id);
if (
item.kind === "assistant_message" ||
(item.kind === "notification" && item.sourceType === "error")
) {
currentResponseItemIds.push(item.id);
}
previousItem = item;
};
@@ -65,7 +68,7 @@ export function deriveStreamTurnTiming(params: {
}
return {
byAssistantId,
byResponseItemId,
runningStartedAt,
};
}
@@ -9348,7 +9348,9 @@ test("turn_failed surfaces provider code and diagnostic in system error message"
await expect(manager.runAgent(agent.id, "hello")).rejects.toThrow("Provider execution failed");
expect(manager.getAgent(agent.id)?.lastError).toBe("Provider execution failed");
expect(manager.getAgent(agent.id)?.lastError).toBe(
"Provider execution failed\n\ncode: 126\n\nNo preset version installed for command claude",
);
const systemError = manager
.getTimeline(agent.id)
@@ -4631,13 +4631,8 @@ export class AgentManager {
if (!isForegroundEvent && !agent.activeForegroundTurnId) {
agent.lifecycle = "error";
}
agent.lastError = event.error;
await this.appendSystemErrorTimelineMessage(
agent,
event.provider,
this.formatTurnFailedMessage(event),
options,
);
agent.lastError = this.formatTurnFailedMessage(event);
await this.appendSystemErrorTimelineMessage(agent, event.provider, agent.lastError, options);
this.resolvePendingPermissionsForAgent(agent, event.provider, options, "Turn failed");
if (!isForegroundEvent && !agent.activeForegroundTurnId) {
this.emitState(agent);
@@ -863,56 +863,100 @@ test("a human pasting an agent envelope stays a human message through provider r
});
});
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,
test.each([
{
error: "Requested fake provider failure",
code: "126",
diagnostic: "Provider executable failed",
closeChild: true,
},
{ error: "long failure " + "x".repeat(5000), closeChild: false },
])(
"errored finish notifications preserve a bounded failure while delivery waits ($closeChild)",
async ({ closeChild, ...failure }) => {
const workdir = mkdtempSync(join(tmpdir(), "agent-finish-error-"));
const logger = createTestLogger();
let releaseRead!: () => void;
let signalRead!: () => void;
const blocked = new Promise<void>((resolve) => {
releaseRead = resolve;
});
await expect(manager.runAgent(child.id, "Emit a turn failure")).rejects.toThrow(
"Requested fake provider failure",
);
await vi.waitFor(() =>
const readStarted = new Promise<void>((resolve) => {
signalRead = resolve;
});
class HoldingStorage extends AgentStorage {
blockedAgentId: string | null = null;
override async get(id: string) {
if (id === this.blockedAgentId) {
signalRead();
await blocked;
}
return super.get(id);
}
}
const storage = new HoldingStorage(join(workdir, "agents"), logger);
await storage.initialize();
const prompts: string[] = [];
const manager = new AgentManager({
logger,
registry: storage,
clients: createTestAgentClients({
turnFailure: failure,
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();
storage.blockedAgentId = parent.id;
setupFinishNotification({
agentManager: manager,
agentStorage: storage,
childAgentId: child.id,
callerAgentId: parent.id,
logger,
});
await expect(manager.runAgent(child.id, "Emit a turn failure")).rejects.toThrow(
failure.error,
);
await readStarted;
if (closeChild) await manager.closeAgent(child.id);
releaseRead();
const fullReason = [
failure.error,
...(failure.code ? [`code: ${failure.code}`] : []),
...(failure.diagnostic ? [failure.diagnostic] : []),
].join("\n\n");
const expectedReason =
fullReason.length > 4000
? `${fullReason.slice(0, 4000)}\n[truncated ${fullReason.length - 4000} chars; use get_agent_activity for the full response]`
: fullReason;
await vi.waitFor(() =>
expect(
parseAgentMessage(prompts.find((prompt) => prompt.includes("errored.")) ?? "")?.text,
).toContain(`<agent-error>\n${expectedReason}\n</agent-error>`),
);
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 });
}
});
).not.toContain("<agent-response>");
if (!closeChild) await manager.closeAgent(child.id);
await manager.closeAgent(parent.id);
} finally {
releaseRead();
rmSync(workdir, { recursive: true, force: true });
}
},
);
@@ -434,6 +434,13 @@ interface FinishNotificationBodyInput {
permissionRequest?: AgentPermissionRequest;
}
function limitFinishNotificationMessage(message: string): string {
const text = message.trim();
if (text.length <= FINISH_NOTIFICATION_MESSAGE_LIMIT) return text;
const omitted = text.length - FINISH_NOTIFICATION_MESSAGE_LIMIT;
return `${text.slice(0, FINISH_NOTIFICATION_MESSAGE_LIMIT)}\n[truncated ${omitted} chars; use get_agent_activity for the full response]`;
}
function formatFinishNotificationBody(params: FinishNotificationBodyInput): string {
const statusLine = `Agent ${params.childAgentId} (${params.title}) ${params.reason}.`;
const sections = [statusLine];
@@ -452,20 +459,20 @@ function formatFinishNotificationBody(params: FinishNotificationBodyInput): stri
);
}
if (params.reason === "errored" && params.lastError) {
sections.push(`<agent-error>\n${params.lastError}\n</agent-error>`);
sections.push(
`<agent-error>\n${limitFinishNotificationMessage(params.lastError)}\n</agent-error>`,
);
}
let lastAssistantMessage = params.lastAssistantMessage?.trim();
if (lastAssistantMessage) {
if (lastAssistantMessage.length > FINISH_NOTIFICATION_MESSAGE_LIMIT) {
const omitted = lastAssistantMessage.length - FINISH_NOTIFICATION_MESSAGE_LIMIT;
lastAssistantMessage = `${lastAssistantMessage.slice(0, FINISH_NOTIFICATION_MESSAGE_LIMIT)}\n[truncated ${omitted} chars; use get_agent_activity for the full response]`;
}
sections.push(`<agent-response>\n${lastAssistantMessage}\n</agent-response>`);
if (params.lastAssistantMessage?.trim()) {
sections.push(
`<agent-response>\n${limitFinishNotificationMessage(params.lastAssistantMessage)}\n</agent-response>`,
);
}
return sections.join("\n\n");
}
interface NotifySafelyOptions {
lastError?: string;
terminal?: boolean;
permissionRequest?: AgentPermissionRequest;
}
@@ -510,6 +517,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
async function notify(
reason: FinishNotificationReason,
permissionRequest?: AgentPermissionRequest,
lastError?: string,
): Promise<void> {
const callerRecord = await agentStorage.get(callerAgentId);
if (callerRecord?.archivedAt) {
@@ -523,8 +531,6 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
const title = record?.title ?? childAgentId;
const lastAssistantMessage =
reason === "errored" ? null : await agentManager.getLastAssistantMessage(childAgentId);
const lastError =
reason === "errored" ? agentManager.getAgent(childAgentId)?.lastError : undefined;
const body = formatFinishNotificationBody({
childAgentId,
title,
@@ -554,7 +560,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
if (stopped) return;
if (options.terminal ?? true) stop();
notificationQueue = notificationQueue
.then(() => notify(reason, options.permissionRequest))
.then(() => notify(reason, options.permissionRequest, options.lastError))
.catch((error) => {
logger.error(
{ err: error, childAgentId, callerAgentId, reason },
@@ -580,7 +586,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams):
// Prior terminal state is not this task's outcome. A failure before turn start
// is reported by the dispatch acknowledgement instead of a finish callback.
if (agent.lifecycle === "error" && hasSeenTurn) {
notifySafely("errored");
notifySafely("errored", { lastError: agent.lastError });
return;
}
// Idle is published before the terminal stream event for both completion
@@ -1781,7 +1781,13 @@ describe("ScheduleService", () => {
async (targetType) => {
const manager = new AgentManager({
logger: createTestLogger(),
clients: createTestAgentClients(),
clients: createTestAgentClients({
turnFailure: {
error: "Requested fake provider failure",
code: "126",
diagnostic: "Provider executable failed",
},
}),
registry: agentStorage,
});
const service = createScheduleService({
@@ -1815,7 +1821,7 @@ describe("ScheduleService", () => {
await service.runOnce(schedule.id);
expect((await service.inspect(schedule.id)).runs[0]).toMatchObject({
status: "failed",
error: "Requested fake provider failure",
error: "Requested fake provider failure\n\ncode: 126\n\nProvider executable failed",
});
await manager.closeAgent(agent.id);
},
@@ -52,6 +52,7 @@ interface Deferred<T> {
}
interface FakeAgentSessionOptions {
turnFailure?: TestAgentClientOptions["turnFailure"];
providerName: string;
config: AgentSessionConfig;
supportsMcpServers?: boolean;
@@ -64,6 +65,10 @@ interface FakeAgentSessionOptions {
}
export interface TestAgentClientOptions {
turnFailure?: Pick<
Extract<AgentStreamEvent, { type: "turn_failed" }>,
"error" | "code" | "diagnostic"
>;
beforeCreateSession?: (config: AgentSessionConfig) => Promise<void>;
closeSession?: () => Promise<void>;
onStartTurn?: (prompt: AgentPromptInput, config: AgentSessionConfig) => void;
@@ -343,6 +348,7 @@ class FakeAgentSession implements AgentSession {
private readonly closeSession: (() => Promise<void>) | undefined;
private readonly onStartTurn: TestAgentClientOptions["onStartTurn"];
private readonly beforeTurnComplete: ((prompt: AgentPromptInput) => Promise<void>) | undefined;
private readonly turnFailure: TestAgentClientOptions["turnFailure"];
private readonly historyEvents: readonly AgentStreamEvent[] | undefined;
constructor(options: FakeAgentSessionOptions) {
@@ -358,6 +364,7 @@ class FakeAgentSession implements AgentSession {
this.onStartTurn = options.onStartTurn;
this.beforeTurnComplete = options.beforeTurnComplete;
this.historyEvents = options.historyEvents;
this.turnFailure = options.turnFailure;
this.historyPath = path.join(
tmpdir(),
"paseo-fake-provider-history",
@@ -761,6 +768,7 @@ class FakeAgentSession implements AgentSession {
type: "turn_failed",
provider: this.providerName,
error: "Requested fake provider failure",
...this.turnFailure,
};
await this.appendHistoryEvent(failed);
this.notifySubscribers(failed);
@@ -1235,6 +1243,7 @@ class FakeAgentClient implements AgentClient {
onStartTurn: this.options.onStartTurn,
beforeTurnComplete: this.options.beforeTurnComplete,
historyEvents: this.options.historyEvents,
turnFailure: this.options.turnFailure,
});
}
@@ -1262,6 +1271,7 @@ class FakeAgentClient implements AgentClient {
onStartTurn: this.options.onStartTurn,
beforeTurnComplete: this.options.beforeTurnComplete,
historyEvents: this.options.historyEvents,
turnFailure: this.options.turnFailure,
});
}