aps-agent/server/app/turn-runner.ts

138 lines
5.1 KiB
TypeScript
Raw Normal View History

/**
* 回合执行器:把运行时的 RuntimeEvent 翻译成平台事件,并让工具调用经 Core.invoke 落地。
*
* 这是引擎与平台之间唯一的翻译层(架构设计 5.2「事件」一行):
* text → message.delta
* tool_call → tool.call(参数只发摘要)+ 调 Core.invoke + 把回执送回运行时
* tool_result → tool.result
* approval_required → approval.requested
* error → error
* turn_end → message.completed + run.state(idle)
*
* A4 之后,写能力的判定与确认卡插在「拿到 tool_call」与「调 Core.invoke」之间。
*/
import type { ExecutionReceipt } from '@aps/contracts';
import { digestParams } from '../core/execution/store.js';
import type { EventInput } from './events.js';
import type { RuntimeSession } from '../agent/runtime.js';
export interface TurnRunnerInput {
runtimeSession: RuntimeSession;
sessionId: string;
runId: string;
assistantMessageId: string;
text: string;
/** 事件发布(EventBus.publish) */
publish: (event: EventInput) => void;
/** 工具出口:只能是 Core.invoke */
invoke: (capabilityId: string, args: unknown, idempotencyKey: string) => Promise<ExecutionReceipt>;
signal: AbortSignal;
}
export interface TurnResult {
text: string;
finishReason: 'stop' | 'cancelled' | 'error';
toolCalls: number;
}
export async function runTurn(input: TurnRunnerInput): Promise<TurnResult> {
const { runtimeSession, sessionId, runId, assistantMessageId, publish, invoke, signal } = input;
let emitted = '';
let toolCalls = 0;
let sawError = false;
let lastTurnEndReason: TurnResult['finishReason'] = 'stop';
publish({ sessionId, runId, type: 'run.state', data: { state: 'running' } });
try {
for await (const event of runtimeSession.send(input.text, { signal })) {
switch (event.type) {
case 'text': {
emitted += event.delta;
publish({ sessionId, runId, type: 'message.delta', data: { messageId: assistantMessageId, text: event.delta } });
break;
}
case 'tool_call': {
toolCalls += 1;
publish({
sessionId,
runId,
type: 'tool.call',
data: { callId: event.callId, capabilityId: event.capabilityId, argsDigest: digestParams(event.args) },
});
// 平台模式:这里执行并回填;引擎模式(Pi + 扩展桥)由扩展自己回调平台执行,平台只观察
if (runtimeSession.toolExecution === 'platform') {
// 幂等键用 callId:同一回合重放不会重复执行
const receipt = await invoke(event.capabilityId, event.args, `call-${event.callId}`);
runtimeSession.resolveToolResult(event.callId, receipt);
if (receipt.status === 'failed') {
publish({
sessionId,
runId,
type: 'error',
data: { code: `tool.${receipt.errorKind ?? 'failed'}`, message: toolFailureText(receipt), retryable: true },
});
}
}
break;
}
case 'tool_result': {
// tool.result 只有这一个出处:运行时上报的回执(平台模式由假引擎回显,引擎模式由 Pi 上报)
publish({
sessionId,
runId,
type: 'tool.result',
data: { callId: event.callId, executionId: event.receipt.executionId, status: event.receipt.status },
});
break;
}
case 'approval_required': {
publish({ sessionId, runId, type: 'approval.requested', data: event.request });
break;
}
case 'error': {
publish({
sessionId,
runId,
type: 'error',
data: { code: `engine.${event.kind}`, message: event.message, retryable: event.kind !== 'forbidden', errorKind: event.kind },
});
sawError = true;
break;
}
case 'turn_end': {
lastTurnEndReason = event.reason;
break;
}
default:
break;
}
}
} catch (error) {
sawError = true;
publish({
sessionId,
runId,
type: 'error',
data: { code: 'engine.failed', message: error instanceof Error ? error.message : '引擎执行失败', retryable: true },
});
}
// 结束原因取「最严重的那个」:取消 > 错误 > 正常结束,避免被 turn_end 覆盖回 stop
const finishReason: TurnResult['finishReason'] = signal.aborted ? 'cancelled' : sawError ? 'error' : lastTurnEndReason;
publish({
sessionId,
runId,
type: 'message.completed',
data: { messageId: assistantMessageId, finishReason },
});
publish({ sessionId, runId, type: 'run.state', data: { state: 'idle' } });
return { text: emitted, finishReason, toolCalls };
}
function toolFailureText(receipt: ExecutionReceipt): string {
const detail = receipt.result as { message?: string } | undefined;
return detail?.message ?? `能力调用失败(${receipt.errorKind ?? 'unknown'})`;
}