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

135 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.

/**
* 回合执行器:把运行时的 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) },
});
// 幂等键用 callId:同一回合重放不会重复执行
const receipt = await invoke(event.capabilityId, event.args, `call-${event.callId}`);
publish({
sessionId,
runId,
type: 'tool.result',
data: { callId: event.callId, executionId: receipt.executionId, status: receipt.status },
});
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': {
// 运行时自行上报的回执(真引擎可能直接给),不重复发事件
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'})`;
}