feat: add tool framework foundation (types, registry, executor, shell tool, model types, SOUL.md)

- Task 0: SOUL.md + loadSystemPrompt() in daemon
- Task 1: Tool type definitions (Tool, ToolCall, ToolResult, etc.)
- Task 2: ToolRegistry with Anthropic/OpenAI serialization
- Task 3: ToolExecutor with hooks, timeout, truncation
- Task 4: shell.exec builtin tool
- Task 8: Model types updated for tool use (ToolDefinition, ModelToolCall, etc.)
- Task 15: Model index exports for tool types
This commit is contained in:
William Valentin
2026-02-05 17:39:40 -08:00
parent 32dd3ad728
commit b00706325b
13 changed files with 691 additions and 7 deletions
+68
View File
@@ -0,0 +1,68 @@
import type { ToolResult } from './types.js';
import type { ToolRegistry } from './registry.js';
import type { HookEngine } from '../hooks/engine.js';
export interface ToolExecutorConfig {
defaultTimeoutMs?: number;
maxOutputBytes?: number;
}
export class ToolExecutor {
private registry: ToolRegistry;
private hooks: HookEngine;
private defaultTimeoutMs: number;
private maxOutputBytes: number;
constructor(registry: ToolRegistry, hooks: HookEngine, config?: ToolExecutorConfig) {
this.registry = registry;
this.hooks = hooks;
this.defaultTimeoutMs = config?.defaultTimeoutMs ?? 30_000;
this.maxOutputBytes = config?.maxOutputBytes ?? 51_200;
}
async execute(toolName: string, args: unknown): Promise<ToolResult> {
const tool = this.registry.get(toolName);
if (!tool) {
return { success: false, output: '', error: `Tool '${toolName}' not found` };
}
// Check hooks
const action = this.hooks.getAction(toolName);
if (action === 'confirm') {
const hookResult = await this.hooks.requestConfirmation(
toolName,
args as Record<string, unknown>,
);
if (!hookResult.approved) {
return {
success: false,
output: '',
error: `Tool '${toolName}' denied by user: ${hookResult.reason ?? 'no reason'}`,
};
}
}
// Execute with timeout
try {
const result = await Promise.race([
tool.execute(args),
new Promise<ToolResult>((_, reject) =>
setTimeout(() => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)), this.defaultTimeoutMs)
),
]);
// Truncate output if too large
if (result.output.length > this.maxOutputBytes) {
result.output = result.output.slice(0, this.maxOutputBytes) + '\n[truncated]';
}
return result;
} catch (error) {
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
};
}
}
}