mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
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:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user