feat: custom tool renderers

This commit is contained in:
Zoe
2026-09-25 19:46:00 -05:00
parent 3f81f96bda
commit bb5795e00d
18 changed files with 1294 additions and 65 deletions
+31 -16
View File
@@ -13,6 +13,8 @@ import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/
import path from "path";
import { isRerankingProvider } from "~~/server/utils/ai-provider";
import { generateFileToken } from "~~/server/utils/file-token";
import { serializeToolError } from "~~/utils/tool-errors";
import { readFirecrawlResponse } from "~~/server/utils/firecrawl";
export default defineEventHandler(async (event) => {
await protectRoute(event);
@@ -490,11 +492,16 @@ const fetchUrlTool = tool({
},
body: JSON.stringify({
url,
formats: ["markdown"],
}),
});
const content = await response.json();
const result = await readFirecrawlResponse(response);
if (!result.ok) {
// The SDK needs a rejected execution to emit a tool-error, not a successful result.
return Promise.reject(new Error(result.error));
}
return {
content: content.data.markdown,
content: result.data,
};
},
});
@@ -608,6 +615,7 @@ async function generateResponse(
// Mark failed tool calls
await Promise.all([...activeToolCalls].map(async id => {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({
status: 'failed',
error: { type: ToolCallType.Text, value: 'Generation failed' }
@@ -619,10 +627,12 @@ async function generateResponse(
messageId: message.id,
toolCallId: id,
toolName: null,
status: 'failed',
error: {
type: ToolCallType.Text,
value: 'Generation failed'
}
},
lastUpdatedAt: lastUpdatedAt.toISOString(),
}
})
}));
@@ -891,8 +901,6 @@ async function generateResponse(
})
activeToolCalls.add(dbToolCallId);
activeParts.set(key, { id: part.id, accumulatedContent: '' });
} break;
case 'tool-call': {
let inputType: ToolCallType = ToolCallType.Text;
@@ -997,8 +1005,6 @@ async function generateResponse(
})
activeToolCalls.add(dbToolCallId);
activeParts.set(key, { id: part.id, accumulatedContent: '' });
}
} break;
case 'tool-result': {
@@ -1028,6 +1034,7 @@ async function generateResponse(
})
.where(eq(messageParts.toolCallId, dbToolCallId));
const lastUpdatedAt = new Date();
await topicEvents.emit(topicId, {
type: 'tool-call-delta',
payload: {
@@ -1039,6 +1046,7 @@ async function generateResponse(
value: outputValue,
},
status: 'failed',
lastUpdatedAt: lastUpdatedAt.toISOString(),
}
})
@@ -1048,6 +1056,7 @@ async function generateResponse(
}
if (dbToolCallId) {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({
status: 'completed',
output: {
@@ -1055,9 +1064,11 @@ async function generateResponse(
value: outputValue,
},
}).where(eq(toolCalls.id, dbToolCallId));
if (token.providerMetadata) await db.update(messageParts)
await db.update(messageParts)
.set({
providerOptions: token.providerMetadata,
finished: true,
lastUpdatedAt,
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
})
.where(eq(messageParts.toolCallId, dbToolCallId));
@@ -1072,6 +1083,7 @@ async function generateResponse(
value: outputValue,
},
status: 'completed',
lastUpdatedAt: lastUpdatedAt.toISOString(),
}
})
@@ -1092,8 +1104,8 @@ async function generateResponse(
outputValue = token.error;
break;
case 'object':
outputType = ToolCallType.Json;
outputValue = JSON.stringify(token.error);
outputType = token.error instanceof Error ? ToolCallType.Text : ToolCallType.Json;
outputValue = serializeToolError(token.error);
break;
default:
console.error('Unknown error type', token.error);
@@ -1104,6 +1116,7 @@ async function generateResponse(
const existingDbToolCallId = nativeToDbToolCallId.get(token.toolCallId);
if (existingDbToolCallId && activeToolCalls.has(existingDbToolCallId)) {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({
status: 'failed',
error: {
@@ -1111,9 +1124,11 @@ async function generateResponse(
value: outputValue as string,
}
}).where(eq(toolCalls.id, existingDbToolCallId));
if (token.providerMetadata) await db.update(messageParts)
await db.update(messageParts)
.set({
providerOptions: token.providerMetadata,
finished: true,
lastUpdatedAt,
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
})
.where(eq(messageParts.toolCallId, existingDbToolCallId));
@@ -1123,10 +1138,12 @@ async function generateResponse(
messageId: message.id,
toolCallId: existingDbToolCallId,
toolName: token.toolName,
status: 'failed',
error: {
type: outputType as ToolCallType,
value: outputValue as string,
},
lastUpdatedAt: lastUpdatedAt.toISOString(),
}
})
} else {
@@ -1176,11 +1193,9 @@ async function generateResponse(
type: 'tool-call-start',
payload: {
messageId: message.id,
part,
part: { ...part, toolCall },
}
})
activeParts.set(key, { id: part.id, accumulatedContent: '' });
}
if (existingDbToolCallId) {
+36
View File
@@ -0,0 +1,36 @@
import type { Result } from "../../types/result";
import { z } from "zod";
import { Err, Ok } from "../../types/result";
const scrapeResponseSchema = z.object({
success: z.boolean().optional(),
error: z.union([z.string(), z.object({ message: z.string() })]).optional(),
message: z.string().optional(),
data: z.unknown().optional(),
});
export const readFirecrawlResponse = async (response: Response): Promise<Result<string, string>> => {
const context = `Firecrawl scrape (HTTP ${response.status})`;
let body: unknown;
try {
body = await response.json();
} catch {
return Err(`${context}: received a non-JSON or unreadable response.`);
}
const parsed = scrapeResponseSchema.safeParse(body);
if (!parsed.success) {
return Err(`${context}: received an unexpected response format.`);
}
const payload = parsed.data;
const providerError = typeof payload.error === "string" ? payload.error : payload.error?.message;
if (!response.ok || payload.success === false || providerError) {
return Err(`${context}: ${providerError || payload.message || response.statusText || "Scrape failed without an error message."}`);
}
const content = z.object({ markdown: z.string() }).safeParse(payload.data);
if (!content.success || content.data.markdown.trim() === "") {
return Err(`${context}: no Markdown content was returned for this URL.`);
}
return Ok(content.data.markdown);
};