import type { GatewayToolResultContentBlock } from '@electron/providers/BaseProvider'; import type { AttachedFileMeta, RawMessage, ToolArtifact, ToolErrorInfo, ToolLifecycleStatus, ToolRenderHints, ToolResultPayload, } from '@runtime/shared/chat-model'; type MaybePromise = T | Promise; export type ToolRuntimePreflightStatus = 'ready' | 'blocked'; export type ToolRuntimeTerminalStatus = Exclude; export type ToolRuntimeSource = 'planner' | 'provider' | 'manual'; export type ToolRuntimePhase = 'preflight' | 'execute' | 'normalize'; export interface ToolRuntimeLifecycleEvent { phase: ToolRuntimePhase; toolCallId: string; toolName: string; summary?: string; ok?: boolean; error?: ToolErrorInfo; metadata?: Record; } export interface ToolRuntimeContext { sessionKey?: string; runId?: string; signal?: AbortSignal; workingDirectory?: string; files?: AttachedFileMeta[]; metadata?: Record; emitStatus?: (event: ToolRuntimeLifecycleEvent) => void; } export interface ToolRuntimeInvocation { toolCallId: string; toolName: string; input?: unknown; summary?: string; source?: ToolRuntimeSource; metadata?: Record; } export interface ToolRuntimePreflightResult { ok: boolean; status: ToolRuntimePreflightStatus; toolCallId: string; toolName: string; summary?: string; normalizedInput?: unknown; warnings?: string[]; missing?: string[]; error?: ToolErrorInfo; metadata?: Record; } export interface ToolRuntimeExecutionResult { ok: boolean; status: ToolRuntimeTerminalStatus; toolCallId: string; toolName: string; normalizedInput?: unknown; summary?: string; raw?: unknown; files?: AttachedFileMeta[]; artifacts?: ToolArtifact[]; logs?: Array>; error?: ToolErrorInfo; retryable?: boolean; skillType?: string; renderHints?: ToolRenderHints; metadata?: Record; durationMs: number; } export interface ToolRuntimeNormalizedResult { ok: boolean; status: ToolRuntimeTerminalStatus; toolCallId: string; toolName: string; summary?: string; payload: ToolResultPayload; block: GatewayToolResultContentBlock; transcriptMessage: RawMessage; } export interface ToolRuntimeRunResult { preflight: ToolRuntimePreflightResult; execution: ToolRuntimeExecutionResult; normalized: ToolRuntimeNormalizedResult; } export interface ToolRuntimeAdapter { readonly toolName: string; matchesTool?(toolName: string): boolean; preflight( invocation: ToolRuntimeInvocation, context: ToolRuntimeContext ): MaybePromise; execute( invocation: ToolRuntimeInvocation, context: ToolRuntimeContext ): MaybePromise; normalize?( execution: ToolRuntimeExecutionResult, context: ToolRuntimeContext ): MaybePromise; } function normalizeToolError(error: unknown, fallbackCode = 'tool_runtime_error'): ToolErrorInfo { if (typeof error === 'string') { return { code: fallbackCode, message: error }; } if (error instanceof Error) { return { code: fallbackCode, message: error.message, details: error, }; } return { code: fallbackCode, message: 'Unknown tool runtime error', details: error, }; } function buildFallbackSummary(execution: ToolRuntimeExecutionResult): string { if (execution.summary?.trim()) { return execution.summary; } if (execution.error?.message?.trim()) { return execution.error.message; } return execution.ok ? `Tool ${execution.toolName} completed` : `Tool ${execution.toolName} failed`; } function buildToolResultText(payload: ToolResultPayload, execution: ToolRuntimeExecutionResult): string { if (payload.summary?.trim()) { return payload.summary; } if (typeof payload.error === 'string' && payload.error.trim()) { return payload.error; } if ( payload.error && typeof payload.error === 'object' && 'message' in payload.error && typeof payload.error.message === 'string' && payload.error.message.trim() ) { return payload.error.message; } if (typeof payload.raw === 'string' && payload.raw.trim()) { return payload.raw; } if (payload.logs?.length) { const firstLog = payload.logs[0]; return typeof firstLog === 'string' ? firstLog : JSON.stringify(firstLog); } return buildFallbackSummary(execution); } export function buildToolResultPayload( execution: ToolRuntimeExecutionResult ): ToolResultPayload { const summary = buildFallbackSummary(execution); return { ok: execution.ok, summary, structuredData: execution.raw, files: execution.files, artifacts: execution.artifacts, logs: execution.logs, error: execution.error, retryable: execution.retryable, skillType: execution.skillType, renderHints: execution.renderHints, raw: execution.raw, }; } export function normalizeToolExecutionResult( execution: ToolRuntimeExecutionResult ): ToolRuntimeNormalizedResult { const payload = buildToolResultPayload(execution); const summary = payload.summary ?? buildFallbackSummary(execution); const block: GatewayToolResultContentBlock = { type: 'tool_result', toolCallId: execution.toolCallId, content: buildToolResultText(payload, execution), result: payload, summary, ok: payload.ok, error: payload.error, }; return { ok: execution.ok, status: execution.status, toolCallId: execution.toolCallId, toolName: execution.toolName, summary, payload, block, transcriptMessage: { role: 'tool_result', content: [block], timestamp: Date.now(), toolCallId: execution.toolCallId, toolName: execution.toolName, toolCall: { id: execution.toolCallId, name: execution.toolName, input: execution.normalizedInput, summary, }, toolResult: payload, details: execution.raw, isError: !execution.ok, }, }; } export function createUnsupportedToolPreflightResult( invocation: ToolRuntimeInvocation ): ToolRuntimePreflightResult { return { ok: false, status: 'blocked', toolCallId: invocation.toolCallId, toolName: invocation.toolName, summary: `No adapter registered for ${invocation.toolName}`, error: { code: 'tool_adapter_not_found', message: `No adapter registered for ${invocation.toolName}`, }, }; } export function createBlockedToolExecutionResult( preflight: ToolRuntimePreflightResult ): ToolRuntimeExecutionResult { return { ok: false, status: 'error', toolCallId: preflight.toolCallId, toolName: preflight.toolName, normalizedInput: preflight.normalizedInput, summary: preflight.summary, error: preflight.error ?? { code: 'tool_preflight_blocked', message: preflight.summary || `Tool ${preflight.toolName} was blocked in preflight`, }, metadata: preflight.metadata, durationMs: 0, }; } function toolMatches(adapter: ToolRuntimeAdapter, toolName: string): boolean { return adapter.toolName === toolName || adapter.matchesTool?.(toolName) === true; } function emitStatus( context: ToolRuntimeContext, event: ToolRuntimeLifecycleEvent ): void { context.emitStatus?.(event); } export class ToolRuntime { private readonly adapters: ToolRuntimeAdapter[]; constructor(adapters: ToolRuntimeAdapter[] = []) { this.adapters = [...adapters]; } register(adapter: ToolRuntimeAdapter): void { this.adapters.push(adapter); } listAdapters(): ToolRuntimeAdapter[] { return [...this.adapters]; } resolveAdapter(toolName: string): ToolRuntimeAdapter | null { return this.adapters.find((adapter) => toolMatches(adapter, toolName)) ?? null; } async preflight( invocation: ToolRuntimeInvocation, context: ToolRuntimeContext = {} ): Promise { const adapter = this.resolveAdapter(invocation.toolName); if (!adapter) { const blocked = createUnsupportedToolPreflightResult(invocation); emitStatus(context, { phase: 'preflight', toolCallId: blocked.toolCallId, toolName: blocked.toolName, summary: blocked.summary, ok: blocked.ok, error: blocked.error, }); return blocked; } try { const preflight = await adapter.preflight(invocation, context); emitStatus(context, { phase: 'preflight', toolCallId: preflight.toolCallId, toolName: preflight.toolName, summary: preflight.summary, ok: preflight.ok, error: preflight.error, metadata: preflight.metadata, }); return preflight; } catch (error) { const normalizedError = normalizeToolError(error, 'tool_preflight_failed'); const blocked: ToolRuntimePreflightResult = { ok: false, status: 'blocked', toolCallId: invocation.toolCallId, toolName: invocation.toolName, summary: normalizedError.message, error: normalizedError, }; emitStatus(context, { phase: 'preflight', toolCallId: blocked.toolCallId, toolName: blocked.toolName, summary: blocked.summary, ok: blocked.ok, error: blocked.error, }); return blocked; } } async execute( invocation: ToolRuntimeInvocation, context: ToolRuntimeContext = {}, preflight?: ToolRuntimePreflightResult ): Promise { const resolvedPreflight = preflight ?? await this.preflight(invocation, context); if (!resolvedPreflight.ok || resolvedPreflight.status === 'blocked') { const blocked = createBlockedToolExecutionResult(resolvedPreflight); emitStatus(context, { phase: 'execute', toolCallId: blocked.toolCallId, toolName: blocked.toolName, summary: blocked.summary, ok: blocked.ok, error: blocked.error, metadata: blocked.metadata, }); return blocked; } const adapter = this.resolveAdapter(invocation.toolName); if (!adapter) { const blocked = createBlockedToolExecutionResult( createUnsupportedToolPreflightResult(invocation) ); emitStatus(context, { phase: 'execute', toolCallId: blocked.toolCallId, toolName: blocked.toolName, summary: blocked.summary, ok: blocked.ok, error: blocked.error, }); return blocked; } const preparedInvocation: ToolRuntimeInvocation = { ...invocation, input: resolvedPreflight.normalizedInput ?? invocation.input, }; const startTime = Date.now(); try { const execution = await adapter.execute(preparedInvocation, context); const completed: ToolRuntimeExecutionResult = { ...execution, toolCallId: execution.toolCallId || preparedInvocation.toolCallId, toolName: execution.toolName || preparedInvocation.toolName, normalizedInput: execution.normalizedInput ?? preparedInvocation.input, durationMs: execution.durationMs ?? Date.now() - startTime, }; emitStatus(context, { phase: 'execute', toolCallId: completed.toolCallId, toolName: completed.toolName, summary: completed.summary, ok: completed.ok, error: completed.error, metadata: completed.metadata, }); return completed; } catch (error) { const normalizedError = normalizeToolError(error, 'tool_execute_failed'); const failed: ToolRuntimeExecutionResult = { ok: false, status: 'error', toolCallId: preparedInvocation.toolCallId, toolName: preparedInvocation.toolName, normalizedInput: preparedInvocation.input, summary: normalizedError.message, error: normalizedError, durationMs: Date.now() - startTime, }; emitStatus(context, { phase: 'execute', toolCallId: failed.toolCallId, toolName: failed.toolName, summary: failed.summary, ok: failed.ok, error: failed.error, }); return failed; } } async normalize( execution: ToolRuntimeExecutionResult, context: ToolRuntimeContext = {} ): Promise { const adapter = this.resolveAdapter(execution.toolName); const normalized = adapter?.normalize ? await adapter.normalize(execution, context) : normalizeToolExecutionResult(execution); emitStatus(context, { phase: 'normalize', toolCallId: normalized.toolCallId, toolName: normalized.toolName, summary: normalized.summary, ok: normalized.ok, error: typeof normalized.payload.error === 'string' ? { message: normalized.payload.error } : normalized.payload.error, }); return normalized; } async run( invocation: ToolRuntimeInvocation, context: ToolRuntimeContext = {} ): Promise { const preflight = await this.preflight(invocation, context); const execution = await this.execute(invocation, context, preflight); const normalized = await this.normalize(execution, context); return { preflight, execution, normalized, }; } } export function createToolRuntime(adapters: ToolRuntimeAdapter[] = []): ToolRuntime { return new ToolRuntime(adapters); }