- Simplify and clarify all tool descriptions for better model understanding - Enable recursive tool calling - model can now chain multiple tools - Pass tools on follow-up calls so model can call more tools after seeing results - Update tool result message to encourage calling additional tools if needed - Include suggestion in error messages so model knows what to do on failure - Fix StreamingIndicator visibility with explicit colors 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
332 lines
7.5 KiB
TypeScript
332 lines
7.5 KiB
TypeScript
/**
|
|
* Tool Executor - Handles running tools and managing results
|
|
*/
|
|
|
|
import type {
|
|
ToolCall,
|
|
ParsedToolCall,
|
|
ToolResult,
|
|
ToolRegistryEntry,
|
|
ToolDefinition,
|
|
ToolContext,
|
|
ToolCallState,
|
|
CustomTool
|
|
} from './types.js';
|
|
import { builtinTools, getBuiltinToolDefinitions } from './builtin.js';
|
|
|
|
/**
|
|
* Execute a custom JavaScript tool
|
|
*
|
|
* SECURITY NOTE: This intentionally executes user-provided JavaScript code.
|
|
* This is by design - users create custom tools with their own code.
|
|
* The code runs in the browser context with the user's own permissions.
|
|
* This is similar to browser DevTools console - users execute their own code.
|
|
*/
|
|
async function executeJavaScriptTool(tool: CustomTool, args: Record<string, unknown>): Promise<unknown> {
|
|
if (!tool.code) {
|
|
throw new Error('JavaScript tool has no code');
|
|
}
|
|
|
|
try {
|
|
// Create an async function to support await in tool code
|
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
const fn = new AsyncFunction('args', `
|
|
"use strict";
|
|
${tool.code}
|
|
`);
|
|
return await fn(args);
|
|
} catch (error) {
|
|
throw new Error(`Tool execution error: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a custom HTTP tool
|
|
*/
|
|
async function executeHttpTool(tool: CustomTool, args: Record<string, unknown>): Promise<unknown> {
|
|
if (!tool.endpoint) {
|
|
throw new Error('HTTP tool has no endpoint');
|
|
}
|
|
|
|
const method = tool.httpMethod || 'POST';
|
|
const url = new URL(tool.endpoint);
|
|
|
|
const options: RequestInit = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
};
|
|
|
|
if (method === 'GET') {
|
|
// Add args as query parameters
|
|
for (const [key, value] of Object.entries(args)) {
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
} else {
|
|
options.body = JSON.stringify(args);
|
|
}
|
|
|
|
const response = await fetch(url.toString(), options);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType?.includes('application/json')) {
|
|
return response.json();
|
|
}
|
|
return response.text();
|
|
}
|
|
|
|
/**
|
|
* Tool Registry - Manages all available tools (builtin + custom)
|
|
*/
|
|
class ToolRegistry {
|
|
private tools: Map<string, ToolRegistryEntry> = new Map();
|
|
|
|
constructor() {
|
|
// Initialize with builtin tools
|
|
for (const [name, entry] of builtinTools) {
|
|
this.tools.set(name, entry);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a custom tool
|
|
*/
|
|
register(name: string, entry: ToolRegistryEntry): void {
|
|
this.tools.set(name, entry);
|
|
}
|
|
|
|
/**
|
|
* Unregister a tool
|
|
*/
|
|
unregister(name: string): boolean {
|
|
const entry = this.tools.get(name);
|
|
if (entry?.isBuiltin) {
|
|
return false; // Cannot unregister builtin tools
|
|
}
|
|
return this.tools.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Get a tool by name
|
|
*/
|
|
get(name: string): ToolRegistryEntry | undefined {
|
|
return this.tools.get(name);
|
|
}
|
|
|
|
/**
|
|
* Check if a tool exists
|
|
*/
|
|
has(name: string): boolean {
|
|
return this.tools.has(name);
|
|
}
|
|
|
|
/**
|
|
* Get all tool definitions (for Ollama API)
|
|
*/
|
|
getDefinitions(): ToolDefinition[] {
|
|
return Array.from(this.tools.values()).map(entry => entry.definition);
|
|
}
|
|
|
|
/**
|
|
* Get builtin tool definitions only
|
|
*/
|
|
getBuiltinDefinitions(): ToolDefinition[] {
|
|
return getBuiltinToolDefinitions();
|
|
}
|
|
|
|
/**
|
|
* Get all tool names
|
|
*/
|
|
getNames(): string[] {
|
|
return Array.from(this.tools.keys());
|
|
}
|
|
|
|
/**
|
|
* Get count of registered tools
|
|
*/
|
|
get size(): number {
|
|
return this.tools.size;
|
|
}
|
|
}
|
|
|
|
/** Singleton registry instance */
|
|
export const toolRegistry = new ToolRegistry();
|
|
|
|
/**
|
|
* Execute a custom tool by its definition
|
|
*/
|
|
export async function executeCustomTool(
|
|
tool: CustomTool,
|
|
args: Record<string, unknown>
|
|
): Promise<unknown> {
|
|
switch (tool.implementation) {
|
|
case 'javascript':
|
|
return executeJavaScriptTool(tool, args);
|
|
case 'http':
|
|
return executeHttpTool(tool, args);
|
|
default:
|
|
throw new Error(`Unknown implementation type: ${tool.implementation}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a tool call from model response
|
|
*/
|
|
export function parseToolCall(call: ToolCall): ParsedToolCall {
|
|
let args: Record<string, unknown> = {};
|
|
|
|
try {
|
|
args = JSON.parse(call.function.arguments);
|
|
} catch {
|
|
// If JSON parsing fails, try to extract as simple value
|
|
args = { value: call.function.arguments };
|
|
}
|
|
|
|
return {
|
|
id: call.id,
|
|
name: call.function.name,
|
|
arguments: args
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run a single tool call
|
|
* @param call - The tool call from the model
|
|
* @param context - Optional execution context
|
|
* @param customTools - Optional array of custom tools to check
|
|
*/
|
|
export async function runToolCall(
|
|
call: ToolCall | ParsedToolCall,
|
|
context?: ToolContext,
|
|
customTools?: CustomTool[]
|
|
): Promise<ToolResult> {
|
|
const parsed = 'function' in call ? parseToolCall(call) : call;
|
|
const { id, name, arguments: args } = parsed;
|
|
|
|
// First check builtin tools in registry
|
|
const entry = toolRegistry.get(name);
|
|
if (entry) {
|
|
try {
|
|
const result = await entry.handler(args);
|
|
|
|
// Check if result is an error object
|
|
if (result && typeof result === 'object' && 'error' in result) {
|
|
const errorObj = result as { error: unknown; suggestion?: string };
|
|
// Include suggestion in error message if present
|
|
const errorMsg = errorObj.suggestion
|
|
? `${String(errorObj.error)}. ${errorObj.suggestion}`
|
|
: String(errorObj.error);
|
|
return {
|
|
toolCallId: id,
|
|
success: false,
|
|
error: errorMsg
|
|
};
|
|
}
|
|
|
|
return {
|
|
toolCallId: id,
|
|
success: true,
|
|
result
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
toolCallId: id,
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error during tool execution'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check custom tools
|
|
if (customTools) {
|
|
const customTool = customTools.find(t => t.name === name && t.enabled);
|
|
if (customTool) {
|
|
try {
|
|
const result = await executeCustomTool(customTool, args);
|
|
return {
|
|
toolCallId: id,
|
|
success: true,
|
|
result
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
toolCallId: id,
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Custom tool execution failed'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
toolCallId: id,
|
|
success: false,
|
|
error: `Unknown tool: ${name}`
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run multiple tool calls in parallel
|
|
*/
|
|
export async function runToolCalls(
|
|
calls: (ToolCall | ParsedToolCall)[],
|
|
context?: ToolContext,
|
|
customTools?: CustomTool[]
|
|
): Promise<ToolResult[]> {
|
|
return Promise.all(calls.map(call => runToolCall(call, context, customTools)));
|
|
}
|
|
|
|
/**
|
|
* Format tool results for inclusion in chat message
|
|
*/
|
|
export function formatToolResultsForChat(results: ToolResult[]): string {
|
|
return results
|
|
.map(result => {
|
|
if (result.success) {
|
|
const value = typeof result.result === 'object'
|
|
? JSON.stringify(result.result, null, 2)
|
|
: String(result.result);
|
|
return `Tool result: ${value}`;
|
|
} else {
|
|
return `Tool error: ${result.error}`;
|
|
}
|
|
})
|
|
.join('\n\n');
|
|
}
|
|
|
|
/**
|
|
* Create a tool call state for UI tracking
|
|
*/
|
|
export function createToolCallState(call: ToolCall | ParsedToolCall): ToolCallState {
|
|
const parsed = 'function' in call ? parseToolCall(call) : call;
|
|
return {
|
|
id: parsed.id,
|
|
name: parsed.name,
|
|
arguments: parsed.arguments,
|
|
status: 'pending',
|
|
startTime: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update tool call state with result
|
|
*/
|
|
export function updateToolCallState(
|
|
state: ToolCallState,
|
|
result: ToolResult
|
|
): ToolCallState {
|
|
return {
|
|
...state,
|
|
status: result.success ? 'success' : 'error',
|
|
result: result.result,
|
|
error: result.error,
|
|
endTime: Date.now()
|
|
};
|
|
}
|