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

138 lines
5.1 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) },
});
// 平台模式:这里执行并回填;引擎模式(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'})`;
}