Files
zn-ai/electron/gateway/tool-runtime.ts
DEV_DSW 4c61e93c3e Add unit tests for skill capabilities, skill planner, and UV setup
- Implement tests for random ID generation, ensuring preference for crypto.randomUUID.
- Create tests for runtime context capabilities, validating the injection of enabled skill capabilities.
- Add tests for skill capability parsing, including classification and command example extraction.
- Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements.
- Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
2026-04-24 17:02:59 +08:00

484 lines
13 KiB
TypeScript

import type { GatewayToolResultContentBlock } from '@electron/providers/BaseProvider';
import type {
AttachedFileMeta,
RawMessage,
ToolArtifact,
ToolErrorInfo,
ToolLifecycleStatus,
ToolRenderHints,
ToolResultPayload,
} from '@runtime/shared/chat-model';
type MaybePromise<T> = T | Promise<T>;
export type ToolRuntimePreflightStatus = 'ready' | 'blocked';
export type ToolRuntimeTerminalStatus = Exclude<ToolLifecycleStatus, 'running'>;
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<string, unknown>;
}
export interface ToolRuntimeContext {
sessionKey?: string;
runId?: string;
signal?: AbortSignal;
workingDirectory?: string;
files?: AttachedFileMeta[];
metadata?: Record<string, unknown>;
emitStatus?: (event: ToolRuntimeLifecycleEvent) => void;
}
export interface ToolRuntimeInvocation {
toolCallId: string;
toolName: string;
input?: unknown;
summary?: string;
source?: ToolRuntimeSource;
metadata?: Record<string, unknown>;
}
export interface ToolRuntimePreflightResult {
ok: boolean;
status: ToolRuntimePreflightStatus;
toolCallId: string;
toolName: string;
summary?: string;
normalizedInput?: unknown;
warnings?: string[];
missing?: string[];
error?: ToolErrorInfo;
metadata?: Record<string, unknown>;
}
export interface ToolRuntimeExecutionResult {
ok: boolean;
status: ToolRuntimeTerminalStatus;
toolCallId: string;
toolName: string;
normalizedInput?: unknown;
summary?: string;
raw?: unknown;
files?: AttachedFileMeta[];
artifacts?: ToolArtifact[];
logs?: Array<string | Record<string, unknown>>;
error?: ToolErrorInfo;
retryable?: boolean;
skillType?: string;
renderHints?: ToolRenderHints;
metadata?: Record<string, unknown>;
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<ToolRuntimePreflightResult>;
execute(
invocation: ToolRuntimeInvocation,
context: ToolRuntimeContext
): MaybePromise<ToolRuntimeExecutionResult>;
normalize?(
execution: ToolRuntimeExecutionResult,
context: ToolRuntimeContext
): MaybePromise<ToolRuntimeNormalizedResult>;
}
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<ToolRuntimePreflightResult> {
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<ToolRuntimeExecutionResult> {
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<ToolRuntimeNormalizedResult> {
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<ToolRuntimeRunResult> {
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);
}