fix(omp): open agent history that contains a failed browser tool call (#5985)

* fix(omp): load history that contains a failed Paseo browser tool result

Paseo browser tools return a structured { code, message, retryable } error in
the tool result details. The OMP result schema passed details.error through
unvalidated while the history mapper treated it as a string, so one failed
browser call threw and the whole agent timeline failed to load.

Closes #5949

* fix(omp): keep the browser tool's structured error in the tool result

Model details.error as a string or a structured { message, ... } error
instead of dropping non-string values, so the expanded tool row keeps
the error code and retryable flag, and read the failure text from the
structured message.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-03 19:23:11 +02:00
committed by GitHub
parent 42806cdbf9
commit 0d05584d04
2 changed files with 90 additions and 3 deletions
@@ -54,6 +54,79 @@ describe("OMP history mapper", () => {
});
});
test("replays a Paseo browser tool result whose details error is an object", async () => {
const events = await collectHistory([
{
role: "assistant",
content: [{ type: "toolCall", id: "shot-1", name: "browser_screenshot", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "shot-1",
toolName: "browser_screenshot",
content: [{ type: "text", text: "The tab has not painted yet. Retry the screenshot." }],
details: {
ok: false,
error: {
code: "screenshot_no_frame",
message: "The tab has not painted yet. Retry the screenshot.",
retryable: true,
},
},
isError: false,
},
{ role: "assistant", content: [{ type: "text", text: "Retrying later." }] },
]);
expect(events.map((event) => event.item)).toEqual([
expect.objectContaining({ type: "tool_call", callId: "shot-1", status: "running" }),
expect.objectContaining({
type: "tool_call",
callId: "shot-1",
name: "browser_screenshot",
detail: expect.objectContaining({
type: "unknown",
output: expect.objectContaining({
details: expect.objectContaining({
error: expect.objectContaining({ code: "screenshot_no_frame", retryable: true }),
}),
}),
}),
}),
expect.objectContaining({ type: "assistant_message", text: "Retrying later." }),
]);
});
test("reports a failed tool's structured details error by its message", async () => {
const events = await collectHistory([
{
role: "assistant",
content: [{ type: "toolCall", id: "tabs-1", name: "browser_list_tabs", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "tabs-1",
toolName: "browser_list_tabs",
content: [],
details: {
ok: false,
error: {
code: "browser_no_host",
message: "No browser automation host is connected.",
retryable: true,
},
},
isError: true,
},
]);
expect(events.at(-1)?.item).toMatchObject({
type: "tool_call",
status: "failed",
error: "No browser automation host is connected.",
});
});
test("restores blocked and abandoned todo state from a session file", async () => {
const dir = mkdtempSync(join(tmpdir(), "omp-todo-state-history-"));
const sessionFile = join(dir, "session.jsonl");
@@ -57,9 +57,13 @@ interface OmpToolResultObject {
details?: OmpToolResultDetails;
}
interface OmpToolResultStructuredError {
message: string;
}
interface OmpToolResultDetails {
diff?: string;
error?: string;
error?: string | OmpToolResultStructuredError;
mode?: string;
xdev?: unknown;
}
@@ -155,9 +159,17 @@ const OmpToolResultContentSchema = z.union([
OmpToolResultUnknownContentSchema,
]);
// OMP's own tools report a string error; Paseo host tools (browser_*) report
// a structured `{ code, message, retryable }` error.
const OmpToolResultErrorSchema = z.union([
z.string(),
z.object({ message: z.string() }).passthrough(),
]);
const OmpToolResultDetailsSchema = z
.object({
diff: z.string().optional(),
error: OmpToolResultErrorSchema.optional(),
})
.passthrough();
@@ -272,8 +284,10 @@ export function extractTextFromToolResult(result: OmpToolResult): string | undef
}
export function toolFailureMessage(result: OmpToolResult): string {
if (result && typeof result !== "string" && result.details?.error) {
return result.details.error.split("\n", 1)[0].slice(0, 240);
const error = result && typeof result !== "string" ? result.details?.error : undefined;
const errorMessage = typeof error === "string" ? error : error?.message;
if (errorMessage) {
return errorMessage.split("\n", 1)[0].slice(0, 240);
}
const output = extractTextFromToolResult(result);
const exitMessage = output?.match(/(?:Command|Process) exited with code \d+/i)?.[0];