aps-agent/server/core/execution/invoke.ts

120 lines
4.8 KiB
TypeScript
Raw Normal View History

/**
* Core.invoke:工具调用的唯一出口(架构设计 4 / 术语表规则 3)。
*
* 职责:解析能力 → 判幂等 → 建执行记录 → 按截止时间调用 Provider → 归一化错误与回执。
* A4 会把策略判定与凭据代理插在「建执行记录」与「调用 Provider」之间;现在留出接缝。
*/
import { randomUUID } from 'node:crypto';
import type { ExecutionReceipt } from '@aps/contracts';
import { isErrorKind, ProviderError, type ErrorKind, type ExecutionContext } from '@aps/provider-sdk';
import type { CapabilityRegistry } from '../registry/registry.js';
import { digestParams, ExecutionStore, type ExecutionRecord } from './store.js';
export interface InvokeRequest {
/** 调用者身份(A4 起由身份上下文补齐,现在只需要用户与租户) */
subject: { userId: string; tenantId: string };
sessionId: string;
projectId?: string | null;
capabilityId: string;
params: unknown;
idempotencyKey?: string;
/** 覆盖默认超时(毫秒) */
timeoutMs?: number;
}
export interface CoreOptions {
registry: CapabilityRegistry;
executions?: ExecutionStore;
defaultTimeoutMs?: number;
}
export interface Core {
invoke(request: InvokeRequest): Promise<ExecutionReceipt>;
executions: ExecutionStore;
registry: CapabilityRegistry;
}
const DEFAULT_TIMEOUT_MS = 30_000;
function failed(executionId: string, errorKind: ErrorKind, message: string): ExecutionReceipt {
return { executionId, status: 'failed', errorKind, result: { message } };
}
/** 把任意异常归一化成六分类之一(实施设计 5) */
function normalizeError(error: unknown): { kind: ErrorKind; message: string } {
if (error instanceof ProviderError) return { kind: error.kind, message: error.message };
if (error instanceof Error) return { kind: 'unknown_result', message: error.message };
return { kind: 'unknown_result', message: 'Provider 抛出未知错误' };
}
export function createCore(options: CoreOptions): Core {
const executions = options.executions ?? new ExecutionStore();
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
async function invoke(request: InvokeRequest): Promise<ExecutionReceipt> {
const capabilityId = request.capabilityId;
if (typeof capabilityId !== 'string' || capabilityId.trim() === '') {
return failed(`exec-${randomUUID()}`, 'invalid_argument', 'capabilityId 不能为空');
}
// 幂等:同一键的重试返回原回执,不重复执行
if (request.idempotencyKey) {
const existing = executions.findByIdempotencyKey(request.idempotencyKey);
if (existing) return existing.receipt;
}
const resolved = options.registry.resolve(capabilityId);
const executionId = `exec-${randomUUID()}`;
if (!resolved) {
return failed(executionId, 'invalid_argument', `能力 ${capabilityId} 未注册`);
}
const timeoutMs = request.timeoutMs ?? defaultTimeoutMs;
const context: ExecutionContext = {
executionId,
sessionId: request.sessionId,
projectId: request.projectId ?? null,
subject: { userId: request.subject.userId, tenantId: request.subject.tenantId },
...(request.idempotencyKey === undefined ? {} : { idempotencyKey: request.idempotencyKey }),
deadlineAt: Date.now() + timeoutMs,
};
executions.create({
executionId,
capabilityId,
providerId: resolved.providerId,
sessionId: request.sessionId,
projectId: request.projectId ?? null,
subjectUserId: request.subject.userId,
paramsDigest: digestParams(request.params),
...(request.idempotencyKey === undefined ? {} : { idempotencyKey: request.idempotencyKey }),
receipt: { executionId, status: 'accepted' },
});
// 超时判定:超时不等于成功也不等于失败——结果未知(实施设计 5)
const timeout = new Promise<ExecutionReceipt>((resolve) => {
setTimeout(() => resolve(failed(executionId, 'unknown_result', `调用超过 ${timeoutMs}ms,结果未知`)), timeoutMs).unref();
});
let receipt: ExecutionReceipt;
try {
receipt = await Promise.race([resolved.provider.invoke(context, { capabilityId, params: request.params }), timeout]);
} catch (error) {
const normalized = normalizeError(error);
receipt = failed(executionId, normalized.kind, normalized.message);
}
// Provider 返回的分类非法时归一为 unknown_result,避免脏值进记录
if (receipt.status === 'failed' && receipt.errorKind !== undefined && !isErrorKind(receipt.errorKind)) {
receipt = failed(executionId, 'unknown_result', `Provider 返回了未知错误分类 ${String(receipt.errorKind)}`);
}
executions.complete(executionId, receipt);
return receipt;
}
return { invoke, executions, registry: options.registry };
}
export type { ExecutionRecord };