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

120 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 };