/** * Wire-model multi-turn runtime kernel(前端编排)。 * Chat / Causal Flow 各自提供「本轮生成」adapter;本模块拥有: * 解析 tool call → 解析 mock candidate → pending gap → incremental_suffix。 * * 为何不在模型后端做 agent loop:仓库只有前端与模型服务,无独立应用后端; * 编排进模型层会混淆职责,且多轮流式需额外轮次信令,而前端每轮 SSE 天然独立。 * Tool config 存前端 Run Draft;模型后端只套 chat template + generate,不感知轮次。 */ import { postCompletionsPromptIncremental } from '../../shared/api/completionsClient'; import { resolveMockTool } from './mockExecutor'; import { parseToolCallFromCompletion } from './toolCallParser'; import type { ToolConfig } from './toolConfig'; import { runMockToolPendingGap, type ToolCallingPendingLine, } from './toolCallingPendingUi'; export const MAX_TOOL_ROUNDS = 16; export function isAbortError(err: unknown): boolean { return err instanceof DOMException && err.name === 'AbortError'; } /** 请求 tool response 的 incremental_suffix(POST /v1/completions/prompt-incremental)。 */ export async function fetchIncrementalSuffix( model: string, enableThinking: boolean, toolName: string, toolContent: string, signal?: AbortSignal, ): Promise { const { incremental_suffix } = await postCompletionsPromptIncremental( { model, tool_content: toolContent, tool_name: toolName, enable_thinking: enableThinking }, { signal }, ); return incremental_suffix; } /** 同步决策:本轮 assistant 产出是否应注入 mock tool response。 */ export type ToolRoundDecision = | { status: 'stop' } | { status: 'malformed' } | { status: 'inject'; toolName: string; mockContent: string }; export function decideToolRoundContinuation( assistantTurnText: string, toolConfig: ToolConfig, ): ToolRoundDecision { const parsed = parseToolCallFromCompletion(assistantTurnText); if (parsed.status === 'malformed') return { status: 'malformed' }; if (parsed.status === 'absent') return { status: 'stop' }; const mockContent = resolveMockTool( toolConfig, parsed.call.name, parsed.call.arguments, ); if (mockContent === null) return { status: 'stop' }; return { status: 'inject', toolName: parsed.call.name, mockContent, }; } export type PrepareToolRoundContinuationOptions = { assistantTurnText: string; toolConfig: ToolConfig; model: string; enableThinking: boolean; signal?: AbortSignal; /** Causal Flow SVG pending;Chat 自管 segment 时可省略 */ mockToolGapUi?: ToolCallingPendingLine; /** mock 已解析、pending gap 之前(Chat 推 pending segment) */ onMockResolved?: () => void; }; export type ToolRoundContinuation = | { status: 'stop' } | { status: 'malformed' } | { status: 'continue'; incrementalSuffix: string }; /** * 在本轮模型产出之后:决定是否继续,并取回应追加到 wire 的 incremental_suffix。 * 不修改 wire——由调用方追加。 */ export async function prepareToolRoundContinuation( opts: PrepareToolRoundContinuationOptions, ): Promise { const decision = decideToolRoundContinuation(opts.assistantTurnText, opts.toolConfig); if (decision.status === 'malformed') return { status: 'malformed' }; if (decision.status === 'stop') return { status: 'stop' }; opts.onMockResolved?.(); await runMockToolPendingGap(opts.signal, opts.mockToolGapUi); const incrementalSuffix = await fetchIncrementalSuffix( opts.model, opts.enableThinking, decision.toolName, decision.mockContent, opts.signal, ); return { status: 'continue', incrementalSuffix }; }