feat(agent): 引擎接口、假引擎与一致性测试套件(A3.1 / A3.2)
runtime.ts 按架构设计 5.2 定义 AgentRuntime / RuntimeSession / RuntimeEvent(text、tool_call、tool_result、approval_required、turn_end、error),补两处实现必需项:createSession 带 sessionId、resolveToolResult 让工具回执回流。fake-runtime 为确定性假引擎,支持脚本与计划器,事件顺序与真引擎一致,用于一致性测试与 A2.7 评测。conformance.ts 是一套跑任何运行时的验收用例:单轮事件顺序、多轮、工具调用与回执、取消、错误分类、带历史恢复。测试 44 → 46 全绿;坏运行时负例证明套件不是摆设。过程中修掉两个自身缺陷:假引擎在 yield 之后才登记回执回调导致回执丢失;一致性套件超时定时器 unref 导致事件循环空转退出。
This commit is contained in:
parent
6fe43f1d4b
commit
89b9535138
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* 引擎一致性测试(任务 A3.2)。
|
||||||
|
*
|
||||||
|
* 同一套用例先跑假引擎,A3.3 接真 Pi 后跑同一套(A3.9 引擎可替换验证也用它)。
|
||||||
|
* 第二个用例故意给一个坏运行时,证明这套检查不是摆设。
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
|
||||||
|
import { runRuntimeConformance, createTestTools } from '@aps/server/agent/conformance';
|
||||||
|
import { createFakeRuntime } from '@aps/server/agent/fake-runtime';
|
||||||
|
|
||||||
|
/** 各场景的脚本:假引擎按这个产出事件 */
|
||||||
|
function turnsFor(scenario) {
|
||||||
|
switch (scenario) {
|
||||||
|
case 'tool_call':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: ['我先查一下。'],
|
||||||
|
toolCalls: [{ capabilityId: 'test.echo', args: { query: '下周排产' } }],
|
||||||
|
afterToolsText: ['查到了,共 3 条。'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
case 'cancel':
|
||||||
|
return [{ text: ['开始处理', '继续处理'] }];
|
||||||
|
case 'error':
|
||||||
|
return [{ error: { kind: 'unavailable', message: '上游不可用' } }];
|
||||||
|
default:
|
||||||
|
return [{ text: ['我是假引擎,'], afterToolsText: [] }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('假引擎通过一致性测试', async () => {
|
||||||
|
const report = await runRuntimeConformance({
|
||||||
|
createRuntime: (scenario) => createFakeRuntime({ name: 'fake', turns: turnsFor(scenario) }),
|
||||||
|
tools: createTestTools(),
|
||||||
|
});
|
||||||
|
assert.equal(report.runtimeName, 'fake');
|
||||||
|
assert.equal(report.ok, true, JSON.stringify(report.checks.filter((check) => !check.ok), null, 2));
|
||||||
|
assert.ok(report.checks.length >= 10, `检查项应有十条以上,实际 ${report.checks.length}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('坏运行时被一致性测试拦下(负例)', async () => {
|
||||||
|
const broken = {
|
||||||
|
name: 'broken',
|
||||||
|
async createSession() {
|
||||||
|
return {
|
||||||
|
id: 'broken-1',
|
||||||
|
async *send(_text, options) {
|
||||||
|
if (options?.signal?.aborted) {
|
||||||
|
// 取消场景:仍然按 stop 收尾,属于违规
|
||||||
|
yield { type: 'turn_end', reason: 'stop' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield { type: 'text', delta: '只说一句' };
|
||||||
|
yield { type: 'turn_end', reason: 'stop' };
|
||||||
|
},
|
||||||
|
resolveToolResult() {
|
||||||
|
/* 不回填:工具回合永远拿不到 tool_result */
|
||||||
|
},
|
||||||
|
abort() {
|
||||||
|
/* 忽略取消 */
|
||||||
|
},
|
||||||
|
async dispose() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = await runRuntimeConformance({ createRuntime: () => broken, tools: createTestTools() });
|
||||||
|
assert.equal(report.ok, false, '坏运行时不应当通过');
|
||||||
|
const failed = report.checks.filter((check) => !check.ok).map((check) => check.name);
|
||||||
|
assert.ok(failed.includes('取消后以 turn_end(cancelled) 结束'), `应检出取消违规,实际:${failed.join('、')}`);
|
||||||
|
assert.ok(failed.includes('回执回流后产出 tool_result'), `应检出工具回执未回流,实际:${failed.join('、')}`);
|
||||||
|
assert.ok(failed.includes('错误事件带合法分类'), `应检出缺少错误事件,实际:${failed.join('、')}`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
/**
|
||||||
|
* 引擎一致性测试套件(任务 A3.2)。
|
||||||
|
*
|
||||||
|
* 同一套用例跑任何运行时实现:多轮对话、工具调用与回执、事件顺序、取消、错误分类、会话恢复
|
||||||
|
* (架构设计 5.2 的验收方式)。FakeRuntime 现在跑,PiRuntime 在 A3.3 接上后跑同一套;
|
||||||
|
* A3.9「引擎可替换验证」也是拿它做判据。
|
||||||
|
*/
|
||||||
|
import type { ExecutionReceipt } from '@aps/contracts';
|
||||||
|
|
||||||
|
import type { AgentRuntime, RuntimeEvent, RuntimeSession, ToolDescriptor } from './runtime.js';
|
||||||
|
|
||||||
|
export type RuntimeScenario = 'text' | 'tool_call' | 'cancel' | 'error';
|
||||||
|
|
||||||
|
export interface ConformanceCheck {
|
||||||
|
name: string;
|
||||||
|
ok: boolean;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConformanceReport {
|
||||||
|
runtimeName: string;
|
||||||
|
ok: boolean;
|
||||||
|
checks: ConformanceCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConformanceOptions {
|
||||||
|
/** 为某个场景创建一个运行时;同一场景可能创建多次 */
|
||||||
|
createRuntime: (scenario: RuntimeScenario) => AgentRuntime | Promise<AgentRuntime>;
|
||||||
|
/** 能力目录投影 */
|
||||||
|
tools: ToolDescriptor[];
|
||||||
|
/** 各场景的输入文本;真引擎用真实问法,假引擎用任意文本 */
|
||||||
|
prompts?: Partial<Record<RuntimeScenario, string>>;
|
||||||
|
/** 单次 send 的超时(毫秒),防止实现挂住测试 */
|
||||||
|
sendTimeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PROMPTS: Record<RuntimeScenario, string> = {
|
||||||
|
text: '你好,介绍一下你自己',
|
||||||
|
tool_call: '查一下下周的排产结果',
|
||||||
|
cancel: '跑一个很长的任务',
|
||||||
|
error: '触发一次错误',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CAPABILITY_FOR_SCENARIO: Partial<Record<RuntimeScenario, string>> = {
|
||||||
|
tool_call: 'test.echo',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createTestTools(): ToolDescriptor[] {
|
||||||
|
return [
|
||||||
|
{ capabilityId: 'test.echo', toolName: 'test_echo', description: '回显', parameters: { type: 'object' } },
|
||||||
|
{ capabilityId: 'test.write', toolName: 'test_write', description: '假写入', parameters: { type: 'object' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单次迭代也要有超时:真引擎挂住时不能让测试跟着挂住 */
|
||||||
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<T>((_resolve, reject) => {
|
||||||
|
setTimeout(() => reject(new Error(`${label} 超过 ${timeoutMs}ms 未返回`)), timeoutMs).unref();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collect(
|
||||||
|
events: AsyncIterable<RuntimeEvent>,
|
||||||
|
timeoutMs: number,
|
||||||
|
hooks?: { onToolCall?: (callId: string) => void },
|
||||||
|
): Promise<RuntimeEvent[]> {
|
||||||
|
const collected: RuntimeEvent[] = [];
|
||||||
|
const iterator = events[Symbol.asyncIterator]();
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const next = await withTimeout(iterator.next(), Math.max(50, deadline - Date.now()), '等待运行时事件');
|
||||||
|
if (next.done) return collected;
|
||||||
|
const event = next.value as RuntimeEvent;
|
||||||
|
collected.push(event);
|
||||||
|
if (event.type === 'tool_call') hooks?.onToolCall?.(event.callId);
|
||||||
|
if (event.type === 'turn_end') return collected;
|
||||||
|
}
|
||||||
|
throw new Error(`send 未在 ${timeoutMs}ms 内结束;已收到:${collected.map((e) => e.type).join(',') || '(无)'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runRuntimeConformance(options: ConformanceOptions): Promise<ConformanceReport> {
|
||||||
|
const timeoutMs = options.sendTimeoutMs ?? 5_000;
|
||||||
|
const prompts = { ...DEFAULT_PROMPTS, ...options.prompts };
|
||||||
|
const checks: ConformanceCheck[] = [];
|
||||||
|
const record = (name: string, ok: boolean, detail?: string) => {
|
||||||
|
checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
|
||||||
|
};
|
||||||
|
|
||||||
|
let runtimeName = '(未知)';
|
||||||
|
let session: RuntimeSession | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = await options.createRuntime('text');
|
||||||
|
runtimeName = runtime.name;
|
||||||
|
session = await runtime.createSession({ sessionId: 'conformance-1', subject: { userId: 'u', tenantId: 't' }, projectId: null, tools: options.tools });
|
||||||
|
record('createSession 返回会话', typeof session.id === 'string' && session.id.length > 0);
|
||||||
|
|
||||||
|
// 1) 单轮文本:事件顺序与结束原因
|
||||||
|
const textEvents = await collect(session.send(prompts.text), timeoutMs);
|
||||||
|
record('单轮产出文本并正常结束', textEvents.some((e) => e.type === 'text') && textEvents.at(-1)?.type === 'turn_end');
|
||||||
|
const last = textEvents.at(-1);
|
||||||
|
record('结束原因为 stop', last?.type === 'turn_end' && last.reason === 'stop', last?.type === 'turn_end' ? `reason=${last.reason}` : '缺少 turn_end');
|
||||||
|
const endIndex = textEvents.findIndex((e) => e.type === 'turn_end');
|
||||||
|
record('turn_end 是最后一个事件', endIndex === textEvents.length - 1, `turn_end 在第 ${endIndex + 1}/${textEvents.length} 个`);
|
||||||
|
|
||||||
|
// 2) 多轮:同一会话连续两轮
|
||||||
|
const secondTurn = await collect(session.send(prompts.text), timeoutMs);
|
||||||
|
record('同一会话可多轮', secondTurn.at(-1)?.type === 'turn_end' && session.id === session.id);
|
||||||
|
} catch (error) {
|
||||||
|
record('基础回合(文本与多轮)', false, error instanceof Error ? error.message : '未知错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 工具调用与回执
|
||||||
|
try {
|
||||||
|
const runtime = await options.createRuntime('tool_call');
|
||||||
|
const toolSession = await runtime.createSession({ sessionId: 'conformance-tools', subject: { userId: 'u', tenantId: 't' }, projectId: null, tools: options.tools });
|
||||||
|
let toolCallSeen = false;
|
||||||
|
let receiptSeen = false;
|
||||||
|
const capabilityId = CAPABILITY_FOR_SCENARIO.tool_call ?? options.tools[0]?.capabilityId ?? '';
|
||||||
|
const events = await collect(toolSession.send(prompts.tool_call), timeoutMs, {
|
||||||
|
onToolCall: (callId) => {
|
||||||
|
toolCallSeen = true;
|
||||||
|
const receipt: ExecutionReceipt = { executionId: `exec-${callId}`, status: 'succeeded', result: { ok: true } };
|
||||||
|
toolSession.resolveToolResult(callId, receipt);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
receiptSeen = events.some((e) => e.type === 'tool_result');
|
||||||
|
const callEvent = events.find((e) => e.type === 'tool_call');
|
||||||
|
record('发出 tool_call 并带能力 id', toolCallSeen && callEvent?.type === 'tool_call' && callEvent.capabilityId === capabilityId, `期望 ${capabilityId}`);
|
||||||
|
record('回执回流后产出 tool_result', receiptSeen);
|
||||||
|
record('工具回合以 turn_end 收尾', events.at(-1)?.type === 'turn_end');
|
||||||
|
await toolSession.dispose();
|
||||||
|
} catch (error) {
|
||||||
|
record('工具调用与回执', false, error instanceof Error ? error.message : '未知错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 取消
|
||||||
|
try {
|
||||||
|
const runtime = await options.createRuntime('cancel');
|
||||||
|
const cancelSession = await runtime.createSession({ sessionId: 'conformance-cancel', subject: { userId: 'u', tenantId: 't' }, projectId: null, tools: options.tools });
|
||||||
|
const controller = new AbortController();
|
||||||
|
const events: RuntimeEvent[] = [];
|
||||||
|
const iterator = cancelSession.send(prompts.cancel, { signal: controller.signal })[Symbol.asyncIterator]();
|
||||||
|
cancelSession.abort();
|
||||||
|
controller.abort();
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const next = await withTimeout(iterator.next(), timeoutMs, '等待取消后的事件');
|
||||||
|
if (next.done) break;
|
||||||
|
events.push(next.value as RuntimeEvent);
|
||||||
|
if ((next.value as RuntimeEvent).type === 'turn_end') break;
|
||||||
|
}
|
||||||
|
const end = events.at(-1);
|
||||||
|
record('取消后以 turn_end(cancelled) 结束', end?.type === 'turn_end' && end.reason === 'cancelled', end?.type === 'turn_end' ? `reason=${end.reason}` : '缺少 turn_end');
|
||||||
|
await cancelSession.dispose();
|
||||||
|
} catch (error) {
|
||||||
|
record('取消', false, error instanceof Error ? error.message : '未知错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 错误分类
|
||||||
|
try {
|
||||||
|
const runtime = await options.createRuntime('error');
|
||||||
|
const errorSession = await runtime.createSession({ sessionId: 'conformance-error', subject: { userId: 'u', tenantId: 't' }, projectId: null, tools: options.tools });
|
||||||
|
const events = await collect(errorSession.send(prompts.error), timeoutMs);
|
||||||
|
const errorEvent = events.find((e) => e.type === 'error');
|
||||||
|
const validKinds = ['invalid_argument', 'forbidden', 'conflict', 'rate_limited', 'unavailable', 'unknown_result'];
|
||||||
|
record(
|
||||||
|
'错误事件带合法分类',
|
||||||
|
errorEvent?.type === 'error' && validKinds.includes(errorEvent.kind),
|
||||||
|
errorEvent?.type === 'error' ? `kind=${errorEvent.kind}` : '未收到 error 事件',
|
||||||
|
);
|
||||||
|
await errorSession.dispose();
|
||||||
|
} catch (error) {
|
||||||
|
record('错误分类', false, error instanceof Error ? error.message : '未知错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) 会话恢复:用平台历史创建新会话并继续
|
||||||
|
try {
|
||||||
|
const runtime = await options.createRuntime('text');
|
||||||
|
const restored = await runtime.createSession({
|
||||||
|
sessionId: 'conformance-restore',
|
||||||
|
subject: { userId: 'u', tenantId: 't' },
|
||||||
|
projectId: null,
|
||||||
|
tools: options.tools,
|
||||||
|
history: [
|
||||||
|
{ messageId: 'm-1', role: 'user', ts: Date.now(), text: '之前的问题' },
|
||||||
|
{ messageId: 'm-2', role: 'assistant', ts: Date.now(), text: '之前的回答' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const events = await collect(restored.send(prompts.text), timeoutMs);
|
||||||
|
record('带历史恢复后仍可对话', events.at(-1)?.type === 'turn_end');
|
||||||
|
await restored.dispose();
|
||||||
|
} catch (error) {
|
||||||
|
record('会话恢复', false, error instanceof Error ? error.message : '未知错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session) await session.dispose();
|
||||||
|
|
||||||
|
return { runtimeName, ok: checks.every((check) => check.ok), checks };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
/**
|
||||||
|
* 假引擎(任务 A3.2):确定性的运行时,用于一致性测试与问法评测(A2.7)。
|
||||||
|
*
|
||||||
|
* 它不做任何「理解」:按脚本或计划器产出事件,形状与真引擎一致。
|
||||||
|
* 这样一致性测试可以断言事件顺序,评测装置也能在无模型的情况下跑通。
|
||||||
|
*/
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
|
import type { ExecutionReceipt } from '@aps/contracts';
|
||||||
|
import type { ErrorKind } from '@aps/provider-sdk';
|
||||||
|
|
||||||
|
import type { AgentRuntime, CreateRuntimeSessionInput, RuntimeEvent, RuntimeSession, SendOptions, ToolDescriptor } from './runtime.js';
|
||||||
|
|
||||||
|
/** 一个回合的脚本:先说话、再调工具、拿到回执后继续说话,或直接报错 */
|
||||||
|
export interface FakeTurn {
|
||||||
|
text?: string[];
|
||||||
|
toolCalls?: Array<{ capabilityId: string; args: unknown }>;
|
||||||
|
afterToolsText?: string[];
|
||||||
|
error?: { kind: ErrorKind; message: string };
|
||||||
|
/** 事件之间的间隔,默认 0(测试要快) */
|
||||||
|
delayMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FakeRuntimeOptions {
|
||||||
|
name?: string;
|
||||||
|
/** 按回合顺序播放;用完后回到默认行为 */
|
||||||
|
turns?: FakeTurn[];
|
||||||
|
/** 或者由计划器决定每个回合怎么走 */
|
||||||
|
planner?: (text: string, tools: ToolDescriptor[]) => FakeTurn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FakeRuntime extends AgentRuntime {
|
||||||
|
stats: { sessions: number; turns: number; aborts: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number) => (ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve());
|
||||||
|
|
||||||
|
export function createFakeRuntime(options: FakeRuntimeOptions = {}): FakeRuntime {
|
||||||
|
const stats = { sessions: 0, turns: 0, aborts: 0 };
|
||||||
|
const scriptedTurns = [...(options.turns ?? [])];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: options.name ?? 'fake',
|
||||||
|
stats,
|
||||||
|
async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
||||||
|
stats.sessions += 1;
|
||||||
|
const id = `fake-${input.sessionId}-${randomUUID().slice(0, 8)}`;
|
||||||
|
const pending = new Map<string, (receipt: ExecutionReceipt) => void>();
|
||||||
|
let aborted = false;
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const nextTurn = (text: string): FakeTurn => {
|
||||||
|
const scripted = scriptedTurns.shift();
|
||||||
|
if (scripted) return scripted;
|
||||||
|
if (options.planner) return options.planner(text, input.tools);
|
||||||
|
return { text: [`(假引擎)收到:${text}`] };
|
||||||
|
};
|
||||||
|
|
||||||
|
async function* runTurn(text: string, sendOptions?: SendOptions): AsyncGenerator<RuntimeEvent> {
|
||||||
|
if (disposed) throw new Error('运行时已释放');
|
||||||
|
aborted = false;
|
||||||
|
stats.turns += 1;
|
||||||
|
const turn = nextTurn(text);
|
||||||
|
const delay = turn.delayMs ?? 0;
|
||||||
|
|
||||||
|
const checkAbort = () => aborted || sendOptions?.signal?.aborted === true;
|
||||||
|
if (checkAbort()) {
|
||||||
|
yield { type: 'turn_end', reason: 'cancelled' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const delta of turn.text ?? []) {
|
||||||
|
yield { type: 'text', delta };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turn.error) {
|
||||||
|
yield { type: 'error', kind: turn.error.kind, message: turn.error.message };
|
||||||
|
yield { type: 'turn_end', reason: 'error' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const call of turn.toolCalls ?? []) {
|
||||||
|
const callId = `call-${randomUUID().slice(0, 8)}`;
|
||||||
|
// 先登记回调再 yield:消费者可能在收到事件的同一刻就把回执送回来
|
||||||
|
const receiptPromise = new Promise<ExecutionReceipt>((resolve) => {
|
||||||
|
pending.set(callId, resolve);
|
||||||
|
});
|
||||||
|
yield { type: 'tool_call', callId, capabilityId: call.capabilityId, args: call.args };
|
||||||
|
|
||||||
|
// 等平台把回执送回来(Pi 走扩展桥,这里走内存回调)
|
||||||
|
const receipt = await receiptPromise;
|
||||||
|
pending.delete(callId);
|
||||||
|
yield { type: 'tool_result', callId, receipt };
|
||||||
|
|
||||||
|
if (checkAbort()) {
|
||||||
|
yield { type: 'turn_end', reason: 'cancelled' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(delay);
|
||||||
|
for (const delta of turn.afterToolsText ?? []) {
|
||||||
|
yield { type: 'text', delta };
|
||||||
|
}
|
||||||
|
|
||||||
|
yield { type: 'turn_end', reason: checkAbort() ? 'cancelled' : 'stop', usage: { turns: 1 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
send(text: string, sendOptions?: SendOptions): AsyncIterable<RuntimeEvent> {
|
||||||
|
return runTurn(text, sendOptions);
|
||||||
|
},
|
||||||
|
resolveToolResult(callId: string, receipt: ExecutionReceipt): void {
|
||||||
|
const resolve = pending.get(callId);
|
||||||
|
if (resolve) resolve(receipt);
|
||||||
|
},
|
||||||
|
abort(): void {
|
||||||
|
stats.aborts += 1;
|
||||||
|
aborted = true;
|
||||||
|
for (const resolve of pending.values()) resolve({ executionId: 'aborted', status: 'failed', errorKind: 'conflict' });
|
||||||
|
pending.clear();
|
||||||
|
},
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
disposed = true;
|
||||||
|
pending.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* 引擎接口(任务 A3.1,架构设计 5.2)。
|
||||||
|
*
|
||||||
|
* 接口只描述「怎么驱动一个循环」,不描述「允许做什么」——权限与门禁全在 Core。
|
||||||
|
* 三个固定点:领域类型、工具出口(Core.invoke)、状态(平台保存)。
|
||||||
|
*
|
||||||
|
* 两个实现:FakeRuntime(A3.2,用于一致性测试与评测)、PiRuntime(A3.3,长驻子进程)。
|
||||||
|
* 与设计文档的两处补充:
|
||||||
|
* 1. createSession 带 sessionId,便于适配层关联平台会话
|
||||||
|
* 2. RuntimeSession 增加 resolveToolResult——工具回执要能回流(Pi 由扩展桥触发,假引擎走内存回调)
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, ExecutionReceipt } from '@aps/contracts';
|
||||||
|
import type { ErrorKind } from '@aps/provider-sdk';
|
||||||
|
|
||||||
|
/** 工具描述:能力目录向引擎的投影(只有引擎需要的那部分) */
|
||||||
|
export interface ToolDescriptor {
|
||||||
|
/** 能力 id,如 aps.schedule.preview */
|
||||||
|
capabilityId: string;
|
||||||
|
/** 引擎侧的工具名(Pi/Codex 的工具名规则不同,由适配层翻译) */
|
||||||
|
toolName: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeUsage {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
turns?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 运行时提出的确认请求:与协议 v1 的 approval.requested 数据一致 */
|
||||||
|
export interface RuntimeApprovalRequest {
|
||||||
|
approvalId: string;
|
||||||
|
capabilityId: string;
|
||||||
|
paramsDigest: string;
|
||||||
|
impactSummary: string;
|
||||||
|
revision: number;
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeEventMap {
|
||||||
|
text: { delta: string };
|
||||||
|
tool_call: { callId: string; capabilityId: string; args: unknown };
|
||||||
|
tool_result: { callId: string; receipt: ExecutionReceipt };
|
||||||
|
approval_required: { request: RuntimeApprovalRequest };
|
||||||
|
turn_end: { reason: 'stop' | 'cancelled' | 'error'; usage?: RuntimeUsage };
|
||||||
|
error: { kind: ErrorKind; message: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一运行时事件:客户端与 Core 都不感知引擎原生协议 */
|
||||||
|
export type RuntimeEvent = {
|
||||||
|
[K in keyof RuntimeEventMap]: { type: K } & RuntimeEventMap[K];
|
||||||
|
}[keyof RuntimeEventMap];
|
||||||
|
|
||||||
|
export type RuntimeEventType = RuntimeEvent['type'];
|
||||||
|
|
||||||
|
export interface CreateRuntimeSessionInput {
|
||||||
|
/** 平台会话 id,用于适配层关联 */
|
||||||
|
sessionId: string;
|
||||||
|
subject: { userId: string; tenantId: string };
|
||||||
|
projectId: string | null;
|
||||||
|
/** 系统提示词;A3.1 起由应用服务提供 */
|
||||||
|
system?: string;
|
||||||
|
/** 能力目录投影 */
|
||||||
|
tools: ToolDescriptor[];
|
||||||
|
/** 平台保存的历史:换进程或重启后据此恢复(架构设计 5.1「会话落库」) */
|
||||||
|
history?: ChatMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendOptions {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeSession {
|
||||||
|
readonly id: string;
|
||||||
|
/** 发一轮输入,产出事件流;流结束表示这一回合结束 */
|
||||||
|
send(text: string, options?: SendOptions): AsyncIterable<RuntimeEvent>;
|
||||||
|
/** 工具执行完回填回执,运行时据此继续生成后续事件 */
|
||||||
|
resolveToolResult(callId: string, receipt: ExecutionReceipt): void;
|
||||||
|
/** 取消当前回合 */
|
||||||
|
abort(): void;
|
||||||
|
/** 释放运行时资源(子进程、连接) */
|
||||||
|
dispose(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentRuntime {
|
||||||
|
readonly name: string;
|
||||||
|
createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession>;
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,26 @@
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./agent/runtime": {
|
||||||
|
"types": "./dist/agent/runtime.d.ts",
|
||||||
|
"default": "./dist/agent/runtime.js"
|
||||||
|
},
|
||||||
|
"./agent/fake-runtime": {
|
||||||
|
"types": "./dist/agent/fake-runtime.d.ts",
|
||||||
|
"default": "./dist/agent/fake-runtime.js"
|
||||||
|
},
|
||||||
|
"./agent/conformance": {
|
||||||
|
"types": "./dist/agent/conformance.d.ts",
|
||||||
|
"default": "./dist/agent/conformance.js"
|
||||||
|
},
|
||||||
|
"./core/execution/invoke": {
|
||||||
|
"types": "./dist/core/execution/invoke.d.ts",
|
||||||
|
"default": "./dist/core/execution/invoke.js"
|
||||||
|
},
|
||||||
|
"./core/registry/registry": {
|
||||||
|
"types": "./dist/core/registry/registry.d.ts",
|
||||||
|
"default": "./dist/core/registry/registry.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue