fix(look-at): catch prompt errors gracefully instead of re-throwing
session.prompt() may throw {} or JSON parse errors even when the server
successfully processes the request. Instead of crashing the tool, catch
all errors and proceed to fetch messages — if the response is available,
return it; otherwise return a clean error string.
This commit is contained in:
parent
3d5abb950e
commit
f22f14d9d1
@ -111,10 +111,10 @@ describe("look-at tool", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("createLookAt error handling", () => {
|
describe("createLookAt error handling", () => {
|
||||||
// given JSON parse error occurs in session.promptAsync
|
// given JSON parse error occurs in session.prompt
|
||||||
// when LookAt tool executed
|
// when LookAt tool executed
|
||||||
// then error propagates (band-aid removed since root cause fixed by promptAsync migration)
|
// then error is caught and messages are still fetched
|
||||||
test("propagates JSON parse error from session.promptAsync", async () => {
|
test("catches JSON parse error and returns assistant message if available", async () => {
|
||||||
const throwingMock = async () => {
|
const throwingMock = async () => {
|
||||||
throw new Error("JSON Parse error: Unexpected EOF")
|
throw new Error("JSON Parse error: Unexpected EOF")
|
||||||
}
|
}
|
||||||
@ -124,6 +124,50 @@ describe("look-at tool", () => {
|
|||||||
create: async () => ({ data: { id: "ses_test_json_error" } }),
|
create: async () => ({ data: { id: "ses_test_json_error" } }),
|
||||||
prompt: throwingMock,
|
prompt: throwingMock,
|
||||||
promptAsync: throwingMock,
|
promptAsync: throwingMock,
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "analysis result" }] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createLookAt({
|
||||||
|
client: mockClient,
|
||||||
|
directory: "/project",
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const toolContext: ToolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "parent-message",
|
||||||
|
agent: "sisyphus",
|
||||||
|
directory: "/project",
|
||||||
|
worktree: "/project",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: () => {},
|
||||||
|
ask: async () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ file_path: "/test/file.png", goal: "analyze image" },
|
||||||
|
toolContext,
|
||||||
|
)
|
||||||
|
expect(result).toBe("analysis result")
|
||||||
|
})
|
||||||
|
|
||||||
|
// given JSON parse error occurs and no messages available
|
||||||
|
// when LookAt tool executed
|
||||||
|
// then returns error string (not throw)
|
||||||
|
test("catches JSON parse error and returns error when no messages", async () => {
|
||||||
|
const throwingMock = async () => {
|
||||||
|
throw new Error("JSON Parse error: Unexpected EOF")
|
||||||
|
}
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
get: async () => ({ data: { directory: "/project" } }),
|
||||||
|
create: async () => ({ data: { id: "ses_test_json_no_msg" } }),
|
||||||
|
prompt: throwingMock,
|
||||||
|
promptAsync: throwingMock,
|
||||||
messages: async () => ({ data: [] }),
|
messages: async () => ({ data: [] }),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -144,15 +188,62 @@ describe("look-at tool", () => {
|
|||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(
|
const result = await tool.execute(
|
||||||
tool.execute({ file_path: "/test/file.png", goal: "analyze image" }, toolContext)
|
{ file_path: "/test/file.png", goal: "analyze image" },
|
||||||
).rejects.toThrow("JSON Parse error: Unexpected EOF")
|
toolContext,
|
||||||
|
)
|
||||||
|
expect(result).toContain("Error")
|
||||||
|
expect(result).toContain("multimodal-looker")
|
||||||
})
|
})
|
||||||
|
|
||||||
// given generic error occurs in session.promptAsync
|
// given empty object error {} thrown (the actual production bug)
|
||||||
// when LookAt tool executed
|
// when LookAt tool executed
|
||||||
// then error propagates
|
// then error is caught gracefully, not re-thrown
|
||||||
test("propagates generic prompt error", async () => {
|
test("catches empty object error from session.prompt", async () => {
|
||||||
|
const throwingMock = async () => {
|
||||||
|
throw {}
|
||||||
|
}
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
get: async () => ({ data: { directory: "/project" } }),
|
||||||
|
create: async () => ({ data: { id: "ses_test_empty_obj" } }),
|
||||||
|
prompt: throwingMock,
|
||||||
|
promptAsync: throwingMock,
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "got it" }] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = createLookAt({
|
||||||
|
client: mockClient,
|
||||||
|
directory: "/project",
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const toolContext: ToolContext = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
messageID: "parent-message",
|
||||||
|
agent: "sisyphus",
|
||||||
|
directory: "/project",
|
||||||
|
worktree: "/project",
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: () => {},
|
||||||
|
ask: async () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ file_path: "/test/file.png", goal: "analyze" },
|
||||||
|
toolContext,
|
||||||
|
)
|
||||||
|
expect(result).toBe("got it")
|
||||||
|
})
|
||||||
|
|
||||||
|
// given generic network error
|
||||||
|
// when LookAt tool executed
|
||||||
|
// then error is caught and returns error string when no messages
|
||||||
|
test("catches generic prompt error and returns error string", async () => {
|
||||||
const throwingMock = async () => {
|
const throwingMock = async () => {
|
||||||
throw new Error("Network connection failed")
|
throw new Error("Network connection failed")
|
||||||
}
|
}
|
||||||
@ -182,9 +273,12 @@ describe("look-at tool", () => {
|
|||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(
|
const result = await tool.execute(
|
||||||
tool.execute({ file_path: "/test/file.pdf", goal: "extract text" }, toolContext)
|
{ file_path: "/test/file.pdf", goal: "extract text" },
|
||||||
).rejects.toThrow("Network connection failed")
|
toolContext,
|
||||||
|
)
|
||||||
|
expect(result).toContain("Error")
|
||||||
|
expect(result).toContain("multimodal-looker")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -126,12 +126,10 @@ Original error: ${createResult.error}`
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (promptError) {
|
} catch (promptError) {
|
||||||
log(`[look_at] Prompt error:`, promptError)
|
log(`[look_at] Prompt error (ignored, will still fetch messages):`, promptError)
|
||||||
|
|
||||||
throw promptError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`[look_at] Prompt sent, fetching messages...`)
|
log(`[look_at] Fetching messages from session ${sessionID}...`)
|
||||||
|
|
||||||
const messagesResult = await ctx.client.session.messages({
|
const messagesResult = await ctx.client.session.messages({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user