fix(tools): return error objects instead of throwing in registry tools

All tool executions now return error objects instead of throwing exceptions.
This allows the AI model to see errors and respond appropriately instead of
causing the entire stream to fail silently.
This commit is contained in:
Ajax Davis 2026-01-25 15:10:52 +10:00
parent 88e66d54ce
commit 9fc928adae
2 changed files with 148 additions and 103 deletions

View file

@ -49,72 +49,101 @@ export const registryExecuteTool = tool({
additionalProperties: false,
}),
async execute({ toolId, params, env }) {
// Parse toolId format: "package::name"
const separatorIndex = toolId.lastIndexOf('::');
if (separatorIndex === -1) {
throw new Error(`Invalid toolId format. Expected "package::name", got "${toolId}"`);
try {
// Parse toolId format: "package::name"
const separatorIndex = toolId.lastIndexOf('::');
if (separatorIndex === -1) {
return {
error: true,
message: `Invalid toolId format. Expected "package::name", got "${toolId}"`,
toolId,
};
}
const packageName = toolId.substring(0, separatorIndex);
const name = toolId.substring(separatorIndex + 2);
if (!packageName || !name) {
return {
error: true,
message: `Invalid toolId format. Expected "package::name", got "${toolId}"`,
toolId,
};
}
// Fetch tool metadata to get version and importUrl
const metaParams = new URLSearchParams({
q: name,
limit: '10',
});
const metaResponse = await fetch(`${TPMJS_API_URL}/api/tools/search?${metaParams}`);
if (!metaResponse.ok) {
return {
error: true,
message: `Failed to fetch tool metadata: ${metaResponse.statusText}`,
toolId,
};
}
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const metaData = (await metaResponse.json()) as any;
const toolsArray = metaData.results?.tools || [];
// Find the exact tool match
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const toolMeta = toolsArray.find(
(t: any) => t.package.npmPackageName === packageName && t.name === name
);
if (!toolMeta) {
return {
error: true,
message: `Tool not found: ${toolId}. Try using registrySearchTool to find available tools.`,
toolId,
};
}
const version = toolMeta.package.npmVersion;
const importUrl = `https://esm.sh/${packageName}@${version}`;
// Execute via sandbox executor
const response = await fetch(`${TPMJS_EXECUTOR_URL}/execute-tool`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
version,
importUrl,
params,
env: env || {},
}),
});
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const result = (await response.json()) as any;
if (!result.success) {
return {
error: true,
message: result.error || 'Tool execution failed',
toolId,
executionTimeMs: result.executionTimeMs,
};
}
return {
toolId,
executionTimeMs: result.executionTimeMs,
output: result.output,
};
} catch (error) {
return {
error: true,
message: error instanceof Error ? error.message : 'Unknown execution error',
toolId,
};
}
const packageName = toolId.substring(0, separatorIndex);
const name = toolId.substring(separatorIndex + 2);
if (!packageName || !name) {
throw new Error(`Invalid toolId format. Expected "package::name", got "${toolId}"`);
}
// Fetch tool metadata to get version and importUrl
const metaParams = new URLSearchParams({
q: name,
limit: '10',
});
const metaResponse = await fetch(`${TPMJS_API_URL}/api/tools/search?${metaParams}`);
if (!metaResponse.ok) {
throw new Error(`Failed to fetch tool metadata: ${metaResponse.statusText}`);
}
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const metaData = (await metaResponse.json()) as any;
const toolsArray = metaData.results?.tools || [];
// Find the exact tool match
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const toolMeta = toolsArray.find(
(t: any) => t.package.npmPackageName === packageName && t.name === name
);
if (!toolMeta) {
throw new Error(`Tool not found: ${toolId}`);
}
const version = toolMeta.package.npmVersion;
const importUrl = `https://esm.sh/${packageName}@${version}`;
// Execute via sandbox executor
const response = await fetch(`${TPMJS_EXECUTOR_URL}/execute-tool`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
name,
version,
importUrl,
params,
env: env || {},
}),
});
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const result = (await response.json()) as any;
if (!result.success) {
throw new Error(result.error || 'Tool execution failed');
}
return {
toolId,
executionTimeMs: result.executionTimeMs,
output: result.output,
};
},
});

View file

@ -58,45 +58,61 @@ export const registrySearchTool = tool({
additionalProperties: false,
}),
async execute({ query, category, limit = 5 }) {
const params = new URLSearchParams({
q: query,
limit: String(limit),
...(category && { category }),
});
try {
const params = new URLSearchParams({
q: query,
limit: String(limit),
...(category && { category }),
});
const url = `${TPMJS_API_URL}/api/tools/search?${params}`;
const response = await fetch(url);
const url = `${TPMJS_API_URL}/api/tools/search?${params}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
if (!response.ok) {
return {
error: true,
message: `Search failed: ${response.statusText}`,
query,
matchCount: 0,
tools: [],
};
}
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const data = (await response.json()) as any;
const toolsArray = data.results?.tools || [];
return {
query,
matchCount: toolsArray.length,
// biome-ignore lint/suspicious/noExplicitAny: Tool types from API vary
tools: toolsArray.map((t: any) => ({
// Unique identifier for registryExecuteTool
toolId: `${t.package.npmPackageName}::${t.name}`,
// Human-readable info
name: t.name,
package: t.package.npmPackageName,
description: t.description,
category: t.package.category,
// Execution requirements
requiredEnvVars:
t.package.env?.filter((e: any) => e.required).map((e: any) => e.name) || [],
// Quality indicators
healthStatus: t.executionHealth,
qualityScore: t.qualityScore,
})),
};
} catch (error) {
return {
error: true,
message: error instanceof Error ? error.message : 'Unknown search error',
query,
matchCount: 0,
tools: [],
};
}
// biome-ignore lint/suspicious/noExplicitAny: API response types vary
const data = (await response.json()) as any;
const toolsArray = data.results?.tools || [];
return {
query,
matchCount: toolsArray.length,
// biome-ignore lint/suspicious/noExplicitAny: Tool types from API vary
tools: toolsArray.map((t: any) => ({
// Unique identifier for registryExecuteTool
toolId: `${t.package.npmPackageName}::${t.name}`,
// Human-readable info
name: t.name,
package: t.package.npmPackageName,
description: t.description,
category: t.package.category,
// Execution requirements
requiredEnvVars:
t.package.env?.filter((e: any) => e.required).map((e: any) => e.name) || [],
// Quality indicators
healthStatus: t.executionHealth,
qualityScore: t.qualityScore,
})),
};
},
});