@huyooo/ai-chat-core 0.2.45 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/index.d.ts +11 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/model-adapter.d.ts +25 -0
- package/dist/adapter/model-adapter.d.ts.map +1 -0
- package/dist/adapter/model-options.d.ts +53 -0
- package/dist/adapter/model-options.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +28 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/chat-runtime.d.ts +96 -0
- package/dist/chat-runtime.d.ts.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/events.d.ts +605 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +1 -1
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.d.ts.map +1 -0
- package/dist/extension/types.d.ts +46 -0
- package/dist/extension/types.d.ts.map +1 -0
- package/dist/families/index.d.ts +11 -0
- package/dist/families/index.d.ts.map +1 -0
- package/dist/families/presets.d.ts +31 -0
- package/dist/families/presets.d.ts.map +1 -0
- package/dist/families/resolver.d.ts +11 -0
- package/dist/families/resolver.d.ts.map +1 -0
- package/dist/families/types.d.ts +29 -0
- package/dist/families/types.d.ts.map +1 -0
- package/dist/governance/command-safety.d.ts +34 -0
- package/dist/governance/command-safety.d.ts.map +1 -0
- package/dist/governance/governance.d.ts +19 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/index.d.ts +12 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/types.d.ts +29 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/index.d.ts +72 -804
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -1
- package/dist/internal/management-args.d.ts +13 -0
- package/dist/internal/management-args.d.ts.map +1 -0
- package/dist/internal/management-results.d.ts +21 -0
- package/dist/internal/management-results.d.ts.map +1 -0
- package/dist/llm-config.d.ts +108 -0
- package/dist/llm-config.d.ts.map +1 -0
- package/dist/logger/core.d.ts +31 -0
- package/dist/logger/core.d.ts.map +1 -0
- package/dist/logger/index.d.ts +9 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/orchestrator/compression-handler.d.ts +29 -0
- package/dist/orchestrator/compression-handler.d.ts.map +1 -0
- package/dist/orchestrator/context-compressor.d.ts +51 -0
- package/dist/orchestrator/context-compressor.d.ts.map +1 -0
- package/dist/orchestrator/context-summarizer.d.ts +41 -0
- package/dist/orchestrator/context-summarizer.d.ts.map +1 -0
- package/dist/orchestrator/index.d.ts +12 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +46 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/types.d.ts +58 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/parts/index.d.ts +13 -0
- package/dist/parts/index.d.ts.map +1 -0
- package/dist/parts/registry.d.ts +11 -0
- package/dist/parts/registry.d.ts.map +1 -0
- package/dist/parts/summaries.d.ts +9 -0
- package/dist/parts/summaries.d.ts.map +1 -0
- package/dist/parts/types.d.ts +61 -0
- package/dist/parts/types.d.ts.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +1 -0
- package/dist/protocols/anthropic.d.ts +20 -0
- package/dist/protocols/anthropic.d.ts.map +1 -0
- package/dist/protocols/ark.d.ts +36 -0
- package/dist/protocols/ark.d.ts.map +1 -0
- package/dist/protocols/deepseek.d.ts +24 -0
- package/dist/protocols/deepseek.d.ts.map +1 -0
- package/dist/protocols/error-utils.d.ts +14 -0
- package/dist/protocols/error-utils.d.ts.map +1 -0
- package/dist/protocols/gemini.d.ts +24 -0
- package/dist/protocols/gemini.d.ts.map +1 -0
- package/dist/protocols/glm.d.ts +20 -0
- package/dist/protocols/glm.d.ts.map +1 -0
- package/dist/protocols/grok.d.ts +20 -0
- package/dist/protocols/grok.d.ts.map +1 -0
- package/dist/protocols/index.d.ts +31 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/minimax.d.ts +38 -0
- package/dist/protocols/minimax.d.ts.map +1 -0
- package/dist/protocols/moonshot.d.ts +20 -0
- package/dist/protocols/moonshot.d.ts.map +1 -0
- package/dist/protocols/openai-sse.d.ts +33 -0
- package/dist/protocols/openai-sse.d.ts.map +1 -0
- package/dist/protocols/openai.d.ts +19 -0
- package/dist/protocols/openai.d.ts.map +1 -0
- package/dist/protocols/qwen.d.ts +26 -0
- package/dist/protocols/qwen.d.ts.map +1 -0
- package/dist/protocols/responses-sse.d.ts +30 -0
- package/dist/protocols/responses-sse.d.ts.map +1 -0
- package/dist/protocols/sse-reader.d.ts +23 -0
- package/dist/protocols/sse-reader.d.ts.map +1 -0
- package/dist/protocols/tool-arguments.d.ts +8 -0
- package/dist/protocols/tool-arguments.d.ts.map +1 -0
- package/dist/protocols/types.d.ts +148 -0
- package/dist/protocols/types.d.ts.map +1 -0
- package/dist/protocols/vercel-gateway.d.ts +15 -0
- package/dist/protocols/vercel-gateway.d.ts.map +1 -0
- package/dist/runtime.d.ts +151 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1 -0
- package/dist/skills/index.d.ts +14 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/management/admin.d.ts +10 -0
- package/dist/skills/management/admin.d.ts.map +1 -0
- package/dist/skills/management/index.d.ts +11 -0
- package/dist/skills/management/index.d.ts.map +1 -0
- package/dist/skills/management/inputs.d.ts +44 -0
- package/dist/skills/management/inputs.d.ts.map +1 -0
- package/dist/skills/management/operations.d.ts +78 -0
- package/dist/skills/management/operations.d.ts.map +1 -0
- package/dist/skills/management/types.d.ts +70 -0
- package/dist/skills/management/types.d.ts.map +1 -0
- package/dist/skills/registry.d.ts +37 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/summaries.d.ts +9 -0
- package/dist/skills/summaries.d.ts.map +1 -0
- package/dist/skills/types.d.ts +61 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/test-utils/mock-sse.d.ts +13 -0
- package/dist/test-utils/mock-sse.d.ts.map +1 -0
- package/dist/tool-manager/define-tool.d.ts +35 -0
- package/dist/tool-manager/define-tool.d.ts.map +1 -0
- package/dist/tool-manager/formats.d.ts +46 -0
- package/dist/tool-manager/formats.d.ts.map +1 -0
- package/dist/tool-manager/identity.d.ts +18 -0
- package/dist/tool-manager/identity.d.ts.map +1 -0
- package/dist/tool-manager/in-process-provider.d.ts +15 -0
- package/dist/tool-manager/in-process-provider.d.ts.map +1 -0
- package/dist/tool-manager/index.d.ts +18 -0
- package/dist/tool-manager/index.d.ts.map +1 -0
- package/dist/tool-manager/manager.d.ts +18 -0
- package/dist/tool-manager/manager.d.ts.map +1 -0
- package/dist/tool-manager/mcp-provider.d.ts +21 -0
- package/dist/tool-manager/mcp-provider.d.ts.map +1 -0
- package/dist/tool-manager/summaries.d.ts +39 -0
- package/dist/tool-manager/summaries.d.ts.map +1 -0
- package/dist/tool-manager/types.d.ts +314 -0
- package/dist/tool-manager/types.d.ts.map +1 -0
- package/dist/types.d.ts +663 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +26 -15
- package/src/adapter/index.ts +25 -0
- package/src/adapter/model-adapter.ts +196 -0
- package/src/adapter/model-options.ts +143 -0
- package/src/adapter/types.ts +41 -0
- package/src/chat-runtime.ts +515 -0
- package/src/constants.ts +9 -102
- package/src/events.ts +364 -150
- package/src/extension/index.ts +24 -0
- package/src/extension/types.ts +49 -0
- package/src/families/index.ts +28 -0
- package/src/families/presets.ts +124 -0
- package/src/families/resolver.ts +22 -0
- package/src/families/types.ts +55 -0
- package/src/governance/command-safety.ts +224 -0
- package/src/governance/governance.ts +125 -0
- package/src/governance/index.ts +38 -0
- package/src/governance/types.ts +44 -0
- package/src/index.ts +250 -145
- package/src/internal/management-args.ts +39 -0
- package/src/internal/management-results.ts +60 -0
- package/src/llm-config.ts +137 -0
- package/src/logger/core.ts +96 -0
- package/src/logger/index.ts +8 -0
- package/src/orchestrator/compression-handler.ts +137 -0
- package/src/{providers → orchestrator}/context-compressor.ts +79 -47
- package/src/orchestrator/context-summarizer.ts +123 -0
- package/src/orchestrator/index.ts +20 -0
- package/src/orchestrator/orchestrator.ts +1002 -0
- package/src/orchestrator/types.ts +70 -0
- package/src/parts/index.ts +20 -0
- package/src/parts/registry.ts +95 -0
- package/src/parts/summaries.ts +40 -0
- package/src/parts/types.ts +63 -0
- package/src/platform.ts +73 -0
- package/src/protocols/anthropic.ts +377 -0
- package/src/protocols/ark.ts +300 -0
- package/src/protocols/deepseek.ts +192 -0
- package/src/{providers/protocols → protocols}/error-utils.ts +17 -20
- package/src/protocols/gemini.ts +352 -0
- package/src/protocols/glm.ts +212 -0
- package/src/protocols/grok.ts +98 -0
- package/src/protocols/index.ts +48 -0
- package/src/protocols/minimax.ts +308 -0
- package/src/protocols/moonshot.ts +186 -0
- package/src/protocols/openai-sse.ts +156 -0
- package/src/protocols/openai.ts +97 -0
- package/src/protocols/qwen.ts +358 -0
- package/src/protocols/responses-sse.ts +224 -0
- package/src/protocols/sse-reader.ts +54 -0
- package/src/protocols/tool-arguments.ts +32 -0
- package/src/{providers/protocols → protocols}/types.ts +46 -37
- package/src/protocols/vercel-gateway.ts +391 -0
- package/src/runtime.ts +167 -0
- package/src/skills/index.ts +29 -0
- package/src/skills/management/admin.ts +170 -0
- package/src/skills/management/index.ts +27 -0
- package/src/skills/management/inputs.ts +79 -0
- package/src/skills/management/operations.ts +256 -0
- package/src/skills/management/types.ts +57 -0
- package/src/skills/registry.ts +120 -0
- package/src/skills/summaries.ts +48 -0
- package/src/skills/types.ts +65 -0
- package/src/test-utils/mock-sse.ts +3 -3
- package/src/tool-manager/define-tool.ts +201 -0
- package/src/tool-manager/formats.ts +146 -0
- package/src/tool-manager/identity.ts +80 -0
- package/src/tool-manager/in-process-provider.ts +164 -0
- package/src/tool-manager/index.ts +63 -0
- package/src/tool-manager/manager.ts +562 -0
- package/src/tool-manager/mcp-provider.ts +509 -0
- package/src/tool-manager/summaries.ts +136 -0
- package/src/tool-manager/types.ts +389 -0
- package/src/types.ts +750 -191
- package/dist/events-CU5D5ray.d.ts +0 -1128
- package/src/agent.ts +0 -409
- package/src/internal/update-plan.ts +0 -2
- package/src/internal/web-search.ts +0 -77
- package/src/mcp/client-manager.ts +0 -302
- package/src/mcp/index.ts +0 -2
- package/src/mcp/types.ts +0 -43
- package/src/providers/context-summarizer.ts +0 -70
- package/src/providers/index.ts +0 -125
- package/src/providers/model-registry.ts +0 -466
- package/src/providers/orchestrator.ts +0 -839
- package/src/providers/protocols/anthropic.ts +0 -406
- package/src/providers/protocols/ark.ts +0 -362
- package/src/providers/protocols/deepseek.ts +0 -344
- package/src/providers/protocols/gemini.ts +0 -350
- package/src/providers/protocols/index.ts +0 -36
- package/src/providers/protocols/openai.ts +0 -420
- package/src/providers/protocols/qwen.ts +0 -315
- package/src/providers/types.ts +0 -264
- package/src/providers/unified-adapter.ts +0 -367
- package/src/router.ts +0 -72
- package/src/tools.ts +0 -162
- package/src/utils.ts +0 -86
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* 直接消费 Protocol 层的 RawEvent,统一处理:
|
|
5
|
+
* 1. Thinking 状态机(首段去换行、done 配对、text 前自动闭合)
|
|
6
|
+
* 2. 工具调用循环 (while iterations < MAX)
|
|
7
|
+
* 3. 消息历史维护 (assistant + tool 消息)
|
|
8
|
+
* 4. ChatEvent 发射
|
|
9
|
+
*
|
|
10
|
+
* 只有两套类型:RawEvent(输入)和 ChatEvent(输出)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ChatEvent } from '../events';
|
|
14
|
+
import type { Tool, ToolErrorCategory, ToolGovernanceSnapshot } from '../types';
|
|
15
|
+
import {
|
|
16
|
+
isToolError,
|
|
17
|
+
normalizeToolResult,
|
|
18
|
+
resolveToolGovernanceSnapshot,
|
|
19
|
+
resolveToolResultUi,
|
|
20
|
+
serializeToolResult,
|
|
21
|
+
} from '../types';
|
|
22
|
+
import type { RawEvent, RawToolCall, ProtocolMessage } from '../protocols';
|
|
23
|
+
import type { ProviderAdapter } from '../adapter/types';
|
|
24
|
+
import type {
|
|
25
|
+
OrchestratorConfig,
|
|
26
|
+
OrchestratorContext,
|
|
27
|
+
OrchestratorOptions,
|
|
28
|
+
AutoRunConfig,
|
|
29
|
+
} from './types';
|
|
30
|
+
import { getModelContextConfigFromLLM } from '../adapter';
|
|
31
|
+
import {
|
|
32
|
+
createTextStart,
|
|
33
|
+
createTextDelta,
|
|
34
|
+
createTextEnd,
|
|
35
|
+
createThinkingStart,
|
|
36
|
+
createThinkingDelta,
|
|
37
|
+
createThinkingEnd,
|
|
38
|
+
createToolCallStart,
|
|
39
|
+
createToolCallOutput,
|
|
40
|
+
createToolCallResult,
|
|
41
|
+
createToolCallRequest,
|
|
42
|
+
createDone,
|
|
43
|
+
createAbort,
|
|
44
|
+
createApiError,
|
|
45
|
+
createStepStart,
|
|
46
|
+
createStepEnd,
|
|
47
|
+
createAssistantSegmentStart,
|
|
48
|
+
createAgentStatus,
|
|
49
|
+
} from '../events';
|
|
50
|
+
import type { FinishReason, ToolFailureReason } from '../events';
|
|
51
|
+
import type { ToolApprovalRequestEvent } from '../events';
|
|
52
|
+
import { createModuleLogger } from '../logger';
|
|
53
|
+
import { validateJsonSchemaArgs } from '../tool-manager/define-tool';
|
|
54
|
+
import { type CompactConfig } from './context-compressor';
|
|
55
|
+
import { compactIfNeeded, compressSingleMessageIfNeeded } from './compression-handler';
|
|
56
|
+
import type { LLMConfig } from '../llm-config';
|
|
57
|
+
// 创建模块专用 logger
|
|
58
|
+
const logger = createModuleLogger('Orchestrator');
|
|
59
|
+
|
|
60
|
+
function shouldLogCompiledPrompt() {
|
|
61
|
+
return process.env.NODE_ENV === 'development';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizePositiveLimit(value: number | undefined): number | undefined {
|
|
65
|
+
if (value === undefined || value === null) return undefined;
|
|
66
|
+
if (!Number.isFinite(value)) return undefined;
|
|
67
|
+
if (value <= 0) return undefined;
|
|
68
|
+
return Math.floor(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ParsedToolArgs {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
args: Record<string, unknown>;
|
|
74
|
+
argumentsJson: string;
|
|
75
|
+
result?: string;
|
|
76
|
+
error?: {
|
|
77
|
+
message: string;
|
|
78
|
+
code: string;
|
|
79
|
+
category: ToolErrorCategory;
|
|
80
|
+
retryable: boolean;
|
|
81
|
+
suggestion: string;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 解析工具调用参数(JSON 对象格式)。解析失败不能执行工具,也不能把非法 arguments 写回模型历史。 */
|
|
86
|
+
function parseToolArgs(args: string): ParsedToolArgs {
|
|
87
|
+
const raw = args ?? '';
|
|
88
|
+
const source = raw.trim() ? raw : '{}';
|
|
89
|
+
try {
|
|
90
|
+
const parsed: unknown = JSON.parse(source);
|
|
91
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
92
|
+
throw new Error('function.arguments must be a JSON object');
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
args: parsed as Record<string, unknown>,
|
|
97
|
+
argumentsJson: JSON.stringify(parsed),
|
|
98
|
+
};
|
|
99
|
+
} catch (e) {
|
|
100
|
+
logger.warn({ args: args?.slice(0, 200), error: String(e) }, '工具调用参数 JSON 解析失败');
|
|
101
|
+
const failureReason: ToolFailureReason = 'parse_error';
|
|
102
|
+
const message = '工具调用参数不是合法 JSON 对象,已拒绝执行工具';
|
|
103
|
+
const error = {
|
|
104
|
+
message,
|
|
105
|
+
code: 'INVALID_TOOL_ARGUMENTS_JSON',
|
|
106
|
+
category: 'validation' as const,
|
|
107
|
+
retryable: true,
|
|
108
|
+
suggestion: '请根据工具 inputSchema 重新生成 JSON 对象形式的 function.arguments。',
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
args: {},
|
|
113
|
+
argumentsJson: '{}',
|
|
114
|
+
error,
|
|
115
|
+
result: JSON.stringify(buildToolFailurePayload({
|
|
116
|
+
message,
|
|
117
|
+
failureReason,
|
|
118
|
+
code: error.code,
|
|
119
|
+
category: error.category,
|
|
120
|
+
retryable: error.retryable,
|
|
121
|
+
suggestion: error.suggestion,
|
|
122
|
+
details: raw ? { rawArgumentsPreview: raw.slice(0, 500) } : undefined,
|
|
123
|
+
cause: String(e),
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveApprovalRequirement(input: {
|
|
130
|
+
autoRunConfig?: AutoRunConfig;
|
|
131
|
+
tool?: Tool;
|
|
132
|
+
governance?: ToolGovernanceSnapshot;
|
|
133
|
+
}): {
|
|
134
|
+
needsApproval: boolean;
|
|
135
|
+
reason?: 'manual-mode' | 'tool-policy' | 'destructive-only';
|
|
136
|
+
} {
|
|
137
|
+
const { autoRunConfig, tool, governance } = input;
|
|
138
|
+
|
|
139
|
+
if (autoRunConfig?.mode === 'manual') {
|
|
140
|
+
return { needsApproval: true, reason: 'manual-mode' };
|
|
141
|
+
}
|
|
142
|
+
if (!tool) {
|
|
143
|
+
return { needsApproval: false };
|
|
144
|
+
}
|
|
145
|
+
if (tool.requiresApproval === true || governance?.approvalPolicy === 'manual') {
|
|
146
|
+
return { needsApproval: true, reason: 'tool-policy' };
|
|
147
|
+
}
|
|
148
|
+
if (governance?.approvalPolicy === 'destructive-only' && governance.sideEffectLevel === 'destructive') {
|
|
149
|
+
return { needsApproval: true, reason: 'destructive-only' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { needsApproval: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildToolFailurePayload(input: {
|
|
156
|
+
message: string;
|
|
157
|
+
failureReason: ToolFailureReason;
|
|
158
|
+
code?: string;
|
|
159
|
+
category?: ToolErrorCategory;
|
|
160
|
+
retryable?: boolean;
|
|
161
|
+
suggestion?: string;
|
|
162
|
+
details?: Record<string, unknown>;
|
|
163
|
+
cause?: string;
|
|
164
|
+
stack?: string;
|
|
165
|
+
}) {
|
|
166
|
+
const { message, failureReason, code, category, retryable, suggestion, details, cause, stack } = input;
|
|
167
|
+
const errorDetails = {
|
|
168
|
+
...(details ?? {}),
|
|
169
|
+
...(cause ? { cause } : {}),
|
|
170
|
+
...(stack ? { stack } : {}),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
status: 'error' as const,
|
|
175
|
+
failureReason,
|
|
176
|
+
error: {
|
|
177
|
+
message,
|
|
178
|
+
...(code ? { code } : {}),
|
|
179
|
+
...(category ? { category } : {}),
|
|
180
|
+
...(retryable !== undefined ? { retryable } : {}),
|
|
181
|
+
...(suggestion ? { suggestion } : {}),
|
|
182
|
+
...(Object.keys(errorDetails).length > 0 ? { details: errorDetails } : {}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function inferToolErrorCategory(code: string | undefined, fallback: ToolErrorCategory): ToolErrorCategory {
|
|
188
|
+
if (code === 'INVALID_PARAMS') return 'validation';
|
|
189
|
+
if (code === 'PERMISSION_DENIED' || code === 'TOOL_NOT_ENABLED') return 'permission';
|
|
190
|
+
if (code === 'NETWORK_ERROR') return 'network';
|
|
191
|
+
if (code === 'NOT_FOUND') return 'not_found';
|
|
192
|
+
if (code === 'TIMEOUT') return 'runtime';
|
|
193
|
+
return fallback;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const MODEL_TOOL_FAILURE_NOTICE = 'This tool call failed. In the final answer, explicitly mention the failure or the verified retry outcome; do not report this step as passed unless a later successful retry clearly verified it.';
|
|
197
|
+
|
|
198
|
+
function buildModelToolContent(result: string, success: boolean): string {
|
|
199
|
+
if (success) return result;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(result) as unknown;
|
|
203
|
+
const envelope: Record<string, unknown> = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
204
|
+
? { ...(parsed as Record<string, unknown>) }
|
|
205
|
+
: { value: parsed };
|
|
206
|
+
envelope._modelNotice = MODEL_TOOL_FAILURE_NOTICE;
|
|
207
|
+
return JSON.stringify(envelope);
|
|
208
|
+
} catch {
|
|
209
|
+
return `${MODEL_TOOL_FAILURE_NOTICE}\n${result}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 单个工具执行上下文 */
|
|
214
|
+
interface ToolExecContext {
|
|
215
|
+
toolCall: RawToolCall;
|
|
216
|
+
args: Record<string, unknown>;
|
|
217
|
+
messages: ProtocolMessage[];
|
|
218
|
+
context: OrchestratorContext;
|
|
219
|
+
options: OrchestratorOptions;
|
|
220
|
+
message: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Chat Orchestrator
|
|
225
|
+
*
|
|
226
|
+
* 核心职责:统一处理工具调用循环,所有 Provider 共享相同的逻辑
|
|
227
|
+
*/
|
|
228
|
+
export class ChatOrchestrator {
|
|
229
|
+
private config: OrchestratorConfig;
|
|
230
|
+
|
|
231
|
+
constructor(config: OrchestratorConfig) {
|
|
232
|
+
this.config = { ...config };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 执行聊天
|
|
237
|
+
*
|
|
238
|
+
* @param adapter Provider 适配器
|
|
239
|
+
* @param message 用户消息
|
|
240
|
+
* @param context Orchestrator 上下文
|
|
241
|
+
* @param options 选项
|
|
242
|
+
*/
|
|
243
|
+
async *chat(
|
|
244
|
+
adapter: ProviderAdapter,
|
|
245
|
+
message: string,
|
|
246
|
+
context: OrchestratorContext,
|
|
247
|
+
options: OrchestratorOptions
|
|
248
|
+
): AsyncGenerator<ChatEvent> {
|
|
249
|
+
const startedAt = Date.now();
|
|
250
|
+
const maxIterations = normalizePositiveLimit(options.maxIterations ?? this.config.maxIterations);
|
|
251
|
+
const maxDurationMs = normalizePositiveLimit(options.maxDurationMs ?? this.config.maxDurationMs);
|
|
252
|
+
const maxToolCalls = normalizePositiveLimit(options.maxToolCalls ?? this.config.maxToolCalls);
|
|
253
|
+
const maxTotalTokens = normalizePositiveLimit(options.maxTotalTokens ?? this.config.maxTotalTokens);
|
|
254
|
+
|
|
255
|
+
logger.info({ model: options.model, messagePreview: message.slice(0, 50) + (message.length > 50 ? '...' : '') }, '开始对话');
|
|
256
|
+
if (shouldLogCompiledPrompt()) {
|
|
257
|
+
logger.info({
|
|
258
|
+
model: options.model,
|
|
259
|
+
compiledPrompt: message,
|
|
260
|
+
}, '完整编译 Prompt');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 构建标准化消息列表
|
|
264
|
+
let messages = this.buildMessages(context, message);
|
|
265
|
+
|
|
266
|
+
const ctxConfig = this.config.llmConfig
|
|
267
|
+
? getModelContextConfigFromLLM(options.model, this.config.llmConfig)
|
|
268
|
+
: undefined;
|
|
269
|
+
const compactConfig: CompactConfig = ctxConfig
|
|
270
|
+
? { contextWindowTokens: ctxConfig.contextWindowTokens, maxOutputTokens: ctxConfig.maxOutputTokens }
|
|
271
|
+
: { contextWindowTokens: 128_000, maxOutputTokens: 8192 };
|
|
272
|
+
|
|
273
|
+
// 单条超长消息压缩:最后一条 user 消息如果占满 context 预算,用长上下文模型压缩
|
|
274
|
+
messages = yield* compressSingleMessageIfNeeded(messages, compactConfig, this.config.summarize, this.config.compressionConfig);
|
|
275
|
+
|
|
276
|
+
// 初始压缩:前端传入的历史可能就已经超长,直接复用当前对话模型
|
|
277
|
+
messages = yield* compactIfNeeded(messages, compactConfig, this.config.summarize, options.model);
|
|
278
|
+
|
|
279
|
+
// 全局状态
|
|
280
|
+
let iterations = 0;
|
|
281
|
+
let toolCallsExecuted = 0;
|
|
282
|
+
let finalText = '';
|
|
283
|
+
let finalFinishReason: FinishReason = 'stop';
|
|
284
|
+
let budgetHitReason: FinishReason | undefined;
|
|
285
|
+
// 内容块 ID 生成器
|
|
286
|
+
let blockSeq = 0;
|
|
287
|
+
const nextBlockId = (prefix: string) => `${prefix}-${++blockSeq}`;
|
|
288
|
+
|
|
289
|
+
// Token 使用累积(多轮工具调用时累加)
|
|
290
|
+
let totalUsage: { promptTokens: number; completionTokens: number; totalTokens: number; reasoningTokens: number; cachedTokens: number } = {
|
|
291
|
+
promptTokens: 0, completionTokens: 0, totalTokens: 0, reasoningTokens: 0, cachedTokens: 0,
|
|
292
|
+
};
|
|
293
|
+
let hasUsage = false;
|
|
294
|
+
|
|
295
|
+
// 工具调用循环
|
|
296
|
+
while (true) {
|
|
297
|
+
if (context.signal.aborted) {
|
|
298
|
+
yield createAbort('请求已取消');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (maxDurationMs !== undefined && Date.now() - startedAt >= maxDurationMs) {
|
|
303
|
+
budgetHitReason = 'max_duration';
|
|
304
|
+
finalFinishReason = 'max_duration';
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (maxIterations !== undefined && iterations >= maxIterations) {
|
|
309
|
+
budgetHitReason = 'max_iterations';
|
|
310
|
+
finalFinishReason = 'max_iterations';
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
iterations++;
|
|
315
|
+
|
|
316
|
+
// 每轮迭代前检查是否需要压缩
|
|
317
|
+
if (iterations > 1) {
|
|
318
|
+
messages = yield* compactIfNeeded(messages, compactConfig, this.config.summarize, options.model);
|
|
319
|
+
}
|
|
320
|
+
if (context.refreshTools) {
|
|
321
|
+
context.tools = await context.refreshTools();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const stepStartedAt = Date.now();
|
|
325
|
+
logger.info({ model: options.model, iterations }, `第 ${iterations} 轮开始`);
|
|
326
|
+
|
|
327
|
+
// 发射步骤开始事件(前端可用于展示 Agent 执行进度)
|
|
328
|
+
yield createStepStart(iterations);
|
|
329
|
+
// 告知前端当前阶段:模型正在思考(首次请求 / 工具执行完后新一轮)
|
|
330
|
+
yield createAgentStatus('thinking');
|
|
331
|
+
|
|
332
|
+
// 每轮状态
|
|
333
|
+
let thinkingStartedAt = 0;
|
|
334
|
+
let thinkingStarted = false;
|
|
335
|
+
let thinkingDone = false;
|
|
336
|
+
let textStarted = false;
|
|
337
|
+
let thinkingContent = '';
|
|
338
|
+
let thinkingSignature = '';
|
|
339
|
+
let thinkingBlockId = '';
|
|
340
|
+
let textBlockId = '';
|
|
341
|
+
|
|
342
|
+
// enableThinking 仅传给 ModelAdapter/协议层(是否向厂商请求推理);界面是否展示思考由流里是否出现 thinking_delta 决定,二者解耦
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const pendingToolCalls: RawToolCall[] = [];
|
|
346
|
+
let hasToolCalls = false;
|
|
347
|
+
|
|
348
|
+
const stream = adapter.streamOnce(messages, context.tools, {
|
|
349
|
+
model: options.model,
|
|
350
|
+
enableThinking: options.enableThinking,
|
|
351
|
+
signal: context.signal,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
for await (const event of stream) {
|
|
355
|
+
switch (event.type) {
|
|
356
|
+
// ========== Thinking 状态机(从 ModelAdapter.transformEvents 移入) ==========
|
|
357
|
+
case 'thinking_delta':
|
|
358
|
+
if (!event.delta || thinkingDone) break;
|
|
359
|
+
thinkingContent += event.delta;
|
|
360
|
+
{
|
|
361
|
+
let delta = event.delta;
|
|
362
|
+
if (!thinkingStarted) {
|
|
363
|
+
delta = delta.replace(/^\n+/, '');
|
|
364
|
+
if (!delta) break;
|
|
365
|
+
thinkingStarted = true;
|
|
366
|
+
thinkingStartedAt = Date.now();
|
|
367
|
+
thinkingBlockId = nextBlockId('thinking');
|
|
368
|
+
yield createAgentStatus(null);
|
|
369
|
+
yield createThinkingStart(thinkingBlockId);
|
|
370
|
+
}
|
|
371
|
+
yield createThinkingDelta(thinkingBlockId, delta);
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
case 'thinking_done':
|
|
376
|
+
if (event.thinkingSignature) thinkingSignature = event.thinkingSignature;
|
|
377
|
+
if (thinkingDone) break;
|
|
378
|
+
thinkingDone = true;
|
|
379
|
+
if (thinkingStarted) {
|
|
380
|
+
yield createThinkingEnd(thinkingBlockId, thinkingStartedAt);
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
// ========== 文本 ==========
|
|
385
|
+
case 'text_delta':
|
|
386
|
+
if (event.delta) {
|
|
387
|
+
if (thinkingStarted && !thinkingDone) {
|
|
388
|
+
thinkingDone = true;
|
|
389
|
+
yield createThinkingEnd(thinkingBlockId, thinkingStartedAt);
|
|
390
|
+
}
|
|
391
|
+
if (!textStarted) {
|
|
392
|
+
textStarted = true;
|
|
393
|
+
textBlockId = nextBlockId('text');
|
|
394
|
+
yield createAssistantSegmentStart('step', iterations);
|
|
395
|
+
yield createAgentStatus(null);
|
|
396
|
+
yield createTextStart(textBlockId);
|
|
397
|
+
}
|
|
398
|
+
finalText += event.delta;
|
|
399
|
+
yield createTextDelta(textBlockId, event.delta);
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
|
|
403
|
+
// ========== 工具调用(完整事件) ==========
|
|
404
|
+
case 'tool_call_done':
|
|
405
|
+
if (event.toolCall) {
|
|
406
|
+
pendingToolCalls.push({
|
|
407
|
+
id: event.toolCall.id || '',
|
|
408
|
+
name: event.toolCall.name || '',
|
|
409
|
+
arguments: event.toolCall.arguments || '{}',
|
|
410
|
+
thoughtSignature: event.toolCall.thoughtSignature,
|
|
411
|
+
});
|
|
412
|
+
hasToolCalls = true;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
// ========== 完成 ==========
|
|
417
|
+
case 'done':
|
|
418
|
+
if (thinkingStarted && !thinkingDone) {
|
|
419
|
+
thinkingDone = true;
|
|
420
|
+
yield createThinkingEnd(thinkingBlockId, thinkingStartedAt);
|
|
421
|
+
}
|
|
422
|
+
if (textStarted) {
|
|
423
|
+
yield createTextEnd(textBlockId);
|
|
424
|
+
textStarted = false;
|
|
425
|
+
}
|
|
426
|
+
logger.info({ finishReason: event.finishReason, usage: event.usage, iterations }, `第 ${iterations} 轮完成`);
|
|
427
|
+
if (event.finishReason) {
|
|
428
|
+
finalFinishReason = event.finishReason as FinishReason;
|
|
429
|
+
}
|
|
430
|
+
if (event.finishReason === 'tool_calls') {
|
|
431
|
+
hasToolCalls = true;
|
|
432
|
+
}
|
|
433
|
+
if (event.usage) {
|
|
434
|
+
hasUsage = true;
|
|
435
|
+
totalUsage.promptTokens += event.usage.promptTokens || 0;
|
|
436
|
+
totalUsage.completionTokens += event.usage.completionTokens || 0;
|
|
437
|
+
totalUsage.totalTokens += event.usage.totalTokens || 0;
|
|
438
|
+
totalUsage.reasoningTokens += event.usage.reasoningTokens || 0;
|
|
439
|
+
totalUsage.cachedTokens += event.usage.cachedTokens || 0;
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
// ========== 错误 ==========
|
|
444
|
+
case 'error':
|
|
445
|
+
logger.error({ error: event.error }, '收到 error');
|
|
446
|
+
yield createApiError(event.error ?? '未知错误');
|
|
447
|
+
return;
|
|
448
|
+
|
|
449
|
+
// 忽略中间事件(tool_call_start, tool_call_delta)
|
|
450
|
+
default:
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 处理工具调用
|
|
456
|
+
if (hasToolCalls && pendingToolCalls.length > 0) {
|
|
457
|
+
if (maxIterations !== undefined && iterations >= maxIterations) {
|
|
458
|
+
finalFinishReason = 'max_iterations';
|
|
459
|
+
budgetHitReason = 'max_iterations';
|
|
460
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
if (maxDurationMs !== undefined && Date.now() - startedAt >= maxDurationMs) {
|
|
464
|
+
finalFinishReason = 'max_duration';
|
|
465
|
+
budgetHitReason = 'max_duration';
|
|
466
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
if (maxTotalTokens !== undefined && totalUsage.totalTokens >= maxTotalTokens) {
|
|
470
|
+
finalFinishReason = 'max_tokens';
|
|
471
|
+
budgetHitReason = 'max_tokens';
|
|
472
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
if (maxToolCalls !== undefined && toolCallsExecuted + pendingToolCalls.length > maxToolCalls) {
|
|
476
|
+
finalFinishReason = 'max_tool_calls';
|
|
477
|
+
budgetHitReason = 'max_tool_calls';
|
|
478
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const clientToolNames = context.clientToolNames;
|
|
483
|
+
const clientToolCalls = clientToolNames
|
|
484
|
+
? pendingToolCalls.filter(tc => clientToolNames.has(tc.name))
|
|
485
|
+
: [];
|
|
486
|
+
|
|
487
|
+
// 如果有客户端工具调用,透传给客户端,结束本轮
|
|
488
|
+
if (clientToolCalls.length > 0) {
|
|
489
|
+
logger.info({ toolNames: clientToolCalls.map(tc => tc.name) }, '检测到客户端工具调用,透传给客户端');
|
|
490
|
+
yield createAgentStatus(null);
|
|
491
|
+
|
|
492
|
+
// 发送所有客户端工具调用请求
|
|
493
|
+
for (const toolCall of clientToolCalls) {
|
|
494
|
+
yield createAssistantSegmentStart('tool_call', iterations);
|
|
495
|
+
const parsedArgs = parseToolArgs(toolCall.arguments);
|
|
496
|
+
if (!parsedArgs.ok) {
|
|
497
|
+
yield createToolCallResult(
|
|
498
|
+
toolCall.id,
|
|
499
|
+
toolCall.name,
|
|
500
|
+
parsedArgs.result ?? '',
|
|
501
|
+
false,
|
|
502
|
+
Date.now(),
|
|
503
|
+
parsedArgs.error,
|
|
504
|
+
undefined,
|
|
505
|
+
'parse_error',
|
|
506
|
+
{},
|
|
507
|
+
);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const currentTool = this.config.tools?.get(toolCall.name);
|
|
511
|
+
const effectiveGovernance = await resolveToolGovernanceSnapshot(currentTool, parsedArgs.args);
|
|
512
|
+
yield createToolCallRequest(
|
|
513
|
+
toolCall.id,
|
|
514
|
+
toolCall.name,
|
|
515
|
+
parsedArgs.args,
|
|
516
|
+
currentTool?.ui,
|
|
517
|
+
effectiveGovernance,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const duration = Date.now() - startedAt;
|
|
522
|
+
yield createDone(finalText, 'tool_calls', undefined, duration);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 工具执行阶段
|
|
527
|
+
yield createAgentStatus(null);
|
|
528
|
+
|
|
529
|
+
// 添加 assistant 消息(包含 tool_calls + thinkingContent)到消息历史
|
|
530
|
+
// thinkingContent 对 Kimi/DeepSeek 等 API 是必需的:开启 thinking 时工具调用循环必须回传
|
|
531
|
+
const parsedToolCalls = pendingToolCalls.map(toolCall => ({
|
|
532
|
+
toolCall,
|
|
533
|
+
parsedArgs: parseToolArgs(toolCall.arguments),
|
|
534
|
+
}));
|
|
535
|
+
messages.push({
|
|
536
|
+
role: 'assistant',
|
|
537
|
+
content: finalText,
|
|
538
|
+
toolCalls: parsedToolCalls.map(({ toolCall, parsedArgs }) => ({
|
|
539
|
+
...toolCall,
|
|
540
|
+
arguments: parsedArgs.argumentsJson,
|
|
541
|
+
})),
|
|
542
|
+
...(thinkingContent ? { thinkingContent } : {}),
|
|
543
|
+
...(thinkingSignature ? { thinkingSignature } : {}),
|
|
544
|
+
} satisfies ProtocolMessage);
|
|
545
|
+
|
|
546
|
+
// 逐个执行云端工具(委托到 executeSingleTool)
|
|
547
|
+
for (const { toolCall, parsedArgs } of parsedToolCalls) {
|
|
548
|
+
if (maxDurationMs !== undefined && Date.now() - startedAt >= maxDurationMs) {
|
|
549
|
+
finalFinishReason = 'max_duration';
|
|
550
|
+
budgetHitReason = 'max_duration';
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
if (maxToolCalls !== undefined && toolCallsExecuted >= maxToolCalls) {
|
|
554
|
+
finalFinishReason = 'max_tool_calls';
|
|
555
|
+
budgetHitReason = 'max_tool_calls';
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
yield createAssistantSegmentStart('tool_call', iterations);
|
|
560
|
+
if (!parsedArgs.ok) {
|
|
561
|
+
const toolStartedAt = Date.now();
|
|
562
|
+
yield createToolCallResult(
|
|
563
|
+
toolCall.id,
|
|
564
|
+
toolCall.name,
|
|
565
|
+
parsedArgs.result ?? '',
|
|
566
|
+
false,
|
|
567
|
+
toolStartedAt,
|
|
568
|
+
parsedArgs.error,
|
|
569
|
+
undefined,
|
|
570
|
+
'parse_error',
|
|
571
|
+
{},
|
|
572
|
+
);
|
|
573
|
+
messages.push({
|
|
574
|
+
role: 'tool',
|
|
575
|
+
content: buildModelToolContent(parsedArgs.result ?? '', false),
|
|
576
|
+
toolCallId: toolCall.id,
|
|
577
|
+
toolName: toolCall.name,
|
|
578
|
+
});
|
|
579
|
+
toolCallsExecuted++;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
yield* this.executeSingleTool({
|
|
584
|
+
toolCall, args: parsedArgs.args, messages, context, options, message,
|
|
585
|
+
});
|
|
586
|
+
toolCallsExecuted++;
|
|
587
|
+
|
|
588
|
+
if (context.signal.aborted) {
|
|
589
|
+
yield createAbort('请求已取消');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (budgetHitReason) {
|
|
595
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 发射步骤结束事件
|
|
600
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
601
|
+
|
|
602
|
+
// 清空累积文本,进入下一轮
|
|
603
|
+
finalText = '';
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 没有工具调用,响应完成
|
|
608
|
+
// 空响应检测:模型正常返回但没有产出任何正文
|
|
609
|
+
if (!finalText && !textStarted) {
|
|
610
|
+
logger.warn(
|
|
611
|
+
{
|
|
612
|
+
model: options.model,
|
|
613
|
+
thinkingOnly: thinkingStarted,
|
|
614
|
+
finishReason: finalFinishReason,
|
|
615
|
+
toolsCount: context.tools?.length ?? 0,
|
|
616
|
+
},
|
|
617
|
+
'模型返回空响应',
|
|
618
|
+
);
|
|
619
|
+
const hint = '模型未生成正文,请重试或更换模型。';
|
|
620
|
+
const hintBlockId = nextBlockId('text');
|
|
621
|
+
yield createAssistantSegmentStart('step', iterations);
|
|
622
|
+
yield createTextStart(hintBlockId);
|
|
623
|
+
yield createTextDelta(hintBlockId, hint);
|
|
624
|
+
yield createTextEnd(hintBlockId);
|
|
625
|
+
finalText = hint;
|
|
626
|
+
}
|
|
627
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
628
|
+
break;
|
|
629
|
+
|
|
630
|
+
} catch (error) {
|
|
631
|
+
// 异常路径也要关闭 step 事件,确保前端 step 计数正确
|
|
632
|
+
yield createStepEnd(iterations, stepStartedAt);
|
|
633
|
+
|
|
634
|
+
if (context.signal.aborted) {
|
|
635
|
+
yield createAbort('请求已取消');
|
|
636
|
+
} else {
|
|
637
|
+
yield createApiError(error instanceof Error ? error.message : String(error));
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (budgetHitReason) {
|
|
644
|
+
logger.warn(
|
|
645
|
+
{
|
|
646
|
+
finishReason: budgetHitReason,
|
|
647
|
+
maxIterations,
|
|
648
|
+
maxDurationMs,
|
|
649
|
+
maxToolCalls,
|
|
650
|
+
maxTotalTokens,
|
|
651
|
+
iterations,
|
|
652
|
+
toolCallsExecuted,
|
|
653
|
+
totalTokens: totalUsage.totalTokens,
|
|
654
|
+
durationMs: Date.now() - startedAt,
|
|
655
|
+
},
|
|
656
|
+
`命中执行预算限制:${budgetHitReason}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
// 发射完成事件(包含累积的 Token 使用统计 + finishReason)
|
|
662
|
+
const duration = Date.now() - startedAt;
|
|
663
|
+
const usage = hasUsage ? {
|
|
664
|
+
promptTokens: totalUsage.promptTokens,
|
|
665
|
+
completionTokens: totalUsage.completionTokens,
|
|
666
|
+
totalTokens: totalUsage.totalTokens,
|
|
667
|
+
...(totalUsage.reasoningTokens > 0 ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
|
|
668
|
+
...(totalUsage.cachedTokens > 0 ? { cachedTokens: totalUsage.cachedTokens } : {}),
|
|
669
|
+
} : undefined;
|
|
670
|
+
logger.info({ model: options.model, duration: `${duration}ms`, iterations, tokens: usage?.totalTokens, finishReason: finalFinishReason }, '对话完成');
|
|
671
|
+
yield createDone(finalText, finalFinishReason, usage, duration);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* 构建标准化消息列表
|
|
676
|
+
*/
|
|
677
|
+
private buildMessages(context: OrchestratorContext, message: string): ProtocolMessage[] {
|
|
678
|
+
const messages: ProtocolMessage[] = [];
|
|
679
|
+
|
|
680
|
+
// 系统提示
|
|
681
|
+
if (context.systemPrompt) {
|
|
682
|
+
messages.push({
|
|
683
|
+
role: 'system',
|
|
684
|
+
content: context.systemPrompt,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// 历史消息
|
|
689
|
+
for (const msg of context.history) {
|
|
690
|
+
const standardMsg: ProtocolMessage = {
|
|
691
|
+
role: msg.role as 'user' | 'assistant' | 'tool' | 'system',
|
|
692
|
+
content: msg.content,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
if (msg.tool_calls) {
|
|
696
|
+
standardMsg.toolCalls = msg.tool_calls.map(tc => ({
|
|
697
|
+
id: tc.id,
|
|
698
|
+
name: tc.function.name,
|
|
699
|
+
arguments: tc.function.arguments,
|
|
700
|
+
thoughtSignature: tc.thought_signature,
|
|
701
|
+
}));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 处理 tool 消息的 tool_call_id
|
|
705
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
706
|
+
standardMsg.toolCallId = msg.tool_call_id;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
messages.push(standardMsg);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 当前用户消息(跳过空消息,用于工具调用后继续对话)
|
|
713
|
+
if (message) {
|
|
714
|
+
messages.push({
|
|
715
|
+
role: 'user',
|
|
716
|
+
content: message,
|
|
717
|
+
images: context.images,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return messages;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* 执行单个工具:批准检查 → 执行 → 结果处理 → 推送消息到历史
|
|
726
|
+
* 从 chat() 主循环提取,降低嵌套深度
|
|
727
|
+
*/
|
|
728
|
+
private async *executeSingleTool(tc: ToolExecContext): AsyncGenerator<ChatEvent> {
|
|
729
|
+
const { toolCall, args, messages, context, message } = tc;
|
|
730
|
+
const toolStartedAt = Date.now();
|
|
731
|
+
const allowedToolNames = new Set(context.tools.map((tool) => tool.name));
|
|
732
|
+
if (!allowedToolNames.has(toolCall.name)) {
|
|
733
|
+
const descriptor = this.config.getToolDescriptor?.(toolCall.name);
|
|
734
|
+
const knownTool = !!descriptor || !!this.config.tools?.has(toolCall.name);
|
|
735
|
+
const enableRef = descriptor?.alias ?? toolCall.name;
|
|
736
|
+
const retryable = knownTool;
|
|
737
|
+
const message = knownTool
|
|
738
|
+
? `工具 "${toolCall.name}" 存在,但未在当前聊天工具定义中启用`
|
|
739
|
+
: `工具 "${toolCall.name}" 未在当前聊天工具定义中声明`;
|
|
740
|
+
const suggestion = knownTool
|
|
741
|
+
? `请先调用 tool_enable,参数 {"items":[{"ref":"${enableRef}","enabled":true}]};工具定义刷新后再调用 "${enableRef}"。`
|
|
742
|
+
: '请先使用 tool_search 查找可用工具,并只调用当前已声明的工具。';
|
|
743
|
+
const failureReason: ToolFailureReason = 'denied';
|
|
744
|
+
const result = JSON.stringify(buildToolFailurePayload({
|
|
745
|
+
message,
|
|
746
|
+
failureReason,
|
|
747
|
+
code: 'TOOL_NOT_ENABLED',
|
|
748
|
+
category: 'permission',
|
|
749
|
+
retryable,
|
|
750
|
+
suggestion,
|
|
751
|
+
}));
|
|
752
|
+
const toolErrorShape = {
|
|
753
|
+
message,
|
|
754
|
+
code: 'TOOL_NOT_ENABLED',
|
|
755
|
+
category: 'permission',
|
|
756
|
+
retryable,
|
|
757
|
+
};
|
|
758
|
+
yield createToolCallResult(
|
|
759
|
+
toolCall.id,
|
|
760
|
+
toolCall.name,
|
|
761
|
+
result,
|
|
762
|
+
false,
|
|
763
|
+
toolStartedAt,
|
|
764
|
+
toolErrorShape,
|
|
765
|
+
undefined,
|
|
766
|
+
failureReason,
|
|
767
|
+
{},
|
|
768
|
+
);
|
|
769
|
+
messages.push({ role: 'tool', content: buildModelToolContent(result, false), toolCallId: toolCall.id, toolName: toolCall.name });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 获取最新的自动运行配置
|
|
774
|
+
const autoRunConfig = this.config.getAutoRunConfig
|
|
775
|
+
? await this.config.getAutoRunConfig()
|
|
776
|
+
: (tc.options.autoRunConfig || this.config.autoRunConfig);
|
|
777
|
+
|
|
778
|
+
// 批准检查
|
|
779
|
+
const currentTool = this.config.tools?.get(toolCall.name);
|
|
780
|
+
let effectiveGovernance: ToolGovernanceSnapshot;
|
|
781
|
+
let argsPassSchemaValidation = true;
|
|
782
|
+
if (currentTool?.parameters) {
|
|
783
|
+
try {
|
|
784
|
+
validateJsonSchemaArgs(currentTool.parameters, args);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
argsPassSchemaValidation = false;
|
|
787
|
+
logger.warn({ toolName: toolCall.name, error: String(error) }, '工具参数校验失败,跳过审批并交由执行层返回错误');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (argsPassSchemaValidation) {
|
|
791
|
+
try {
|
|
792
|
+
effectiveGovernance = await resolveToolGovernanceSnapshot(currentTool, args);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
logger.warn({ toolName: toolCall.name, error: String(error) }, '工具输入治理预判失败,已提升为 manual');
|
|
795
|
+
effectiveGovernance = { approvalPolicy: 'manual' };
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
effectiveGovernance = currentTool
|
|
799
|
+
? {
|
|
800
|
+
approvalPolicy: currentTool.approvalPolicy,
|
|
801
|
+
sideEffectLevel: currentTool.sideEffectLevel,
|
|
802
|
+
hostDependency: currentTool.hostDependency,
|
|
803
|
+
}
|
|
804
|
+
: {};
|
|
805
|
+
}
|
|
806
|
+
const approval = resolveApprovalRequirement({ autoRunConfig, tool: currentTool, governance: effectiveGovernance });
|
|
807
|
+
const needsApproval = argsPassSchemaValidation && approval.needsApproval;
|
|
808
|
+
if (needsApproval && this.config.onToolApprovalRequest) {
|
|
809
|
+
const toolDescriptor = this.config.getToolDescriptor?.(toolCall.name);
|
|
810
|
+
const approvalToolMeta = {
|
|
811
|
+
toolName: toolDescriptor?.name,
|
|
812
|
+
extensionId: toolDescriptor?.assetId,
|
|
813
|
+
alias: toolDescriptor?.alias,
|
|
814
|
+
displayName: toolDescriptor?.displayName,
|
|
815
|
+
};
|
|
816
|
+
logger.info({ toolName: toolCall.name }, '发送工具批准请求');
|
|
817
|
+
const approvalRequest: ToolApprovalRequestEvent = {
|
|
818
|
+
type: 'tool_approval_request',
|
|
819
|
+
data: {
|
|
820
|
+
id: toolCall.id,
|
|
821
|
+
name: toolCall.name,
|
|
822
|
+
...approvalToolMeta,
|
|
823
|
+
args,
|
|
824
|
+
reason: approval.reason ?? 'tool-policy',
|
|
825
|
+
approvalPolicy: effectiveGovernance.approvalPolicy,
|
|
826
|
+
sideEffectLevel: effectiveGovernance.sideEffectLevel,
|
|
827
|
+
hostDependency: effectiveGovernance.hostDependency,
|
|
828
|
+
...(effectiveGovernance.riskSummary ? { riskSummary: effectiveGovernance.riskSummary } : {}),
|
|
829
|
+
...(effectiveGovernance.riskTags?.length ? { riskTags: effectiveGovernance.riskTags } : {}),
|
|
830
|
+
...(effectiveGovernance.riskSignals?.length ? { riskSignals: effectiveGovernance.riskSignals } : {}),
|
|
831
|
+
requestedAt: Date.now(),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
yield approvalRequest;
|
|
835
|
+
|
|
836
|
+
const approved = await this.config.onToolApprovalRequest({
|
|
837
|
+
id: toolCall.id,
|
|
838
|
+
name: toolCall.name,
|
|
839
|
+
...approvalToolMeta,
|
|
840
|
+
args,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
if (!approved) {
|
|
844
|
+
const failureReason: ToolFailureReason = 'denied';
|
|
845
|
+
const result = JSON.stringify(buildToolFailurePayload({
|
|
846
|
+
message: '用户跳过了此工具',
|
|
847
|
+
failureReason,
|
|
848
|
+
code: 'TOOL_CALL_SKIPPED',
|
|
849
|
+
category: 'permission',
|
|
850
|
+
retryable: true,
|
|
851
|
+
suggestion: '如果需要执行该工具,请重新发起请求并批准工具调用。',
|
|
852
|
+
}));
|
|
853
|
+
const toolErrorShape = {
|
|
854
|
+
message: '用户跳过了此工具',
|
|
855
|
+
code: 'TOOL_CALL_SKIPPED',
|
|
856
|
+
category: 'permission' as const,
|
|
857
|
+
retryable: true,
|
|
858
|
+
suggestion: '如果需要执行该工具,请重新发起请求并批准工具调用。',
|
|
859
|
+
};
|
|
860
|
+
yield createToolCallResult(
|
|
861
|
+
toolCall.id,
|
|
862
|
+
toolCall.name,
|
|
863
|
+
result,
|
|
864
|
+
false,
|
|
865
|
+
toolStartedAt,
|
|
866
|
+
toolErrorShape,
|
|
867
|
+
undefined,
|
|
868
|
+
failureReason,
|
|
869
|
+
effectiveGovernance,
|
|
870
|
+
);
|
|
871
|
+
messages.push({ role: 'tool', content: buildModelToolContent(result, false), toolCallId: toolCall.id, toolName: toolCall.name });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// 发射工具开始事件(携带 ui 声明,前端据此决定渲染方式)
|
|
877
|
+
yield createToolCallStart(
|
|
878
|
+
toolCall.id,
|
|
879
|
+
toolCall.name,
|
|
880
|
+
args,
|
|
881
|
+
currentTool?.ui,
|
|
882
|
+
effectiveGovernance,
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// 超时信号包装
|
|
886
|
+
const toolDefForTimeout = this.config.tools?.get(toolCall.name);
|
|
887
|
+
let effectiveSignal = context.signal;
|
|
888
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
889
|
+
if (toolDefForTimeout?.timeout) {
|
|
890
|
+
const ac = new AbortController();
|
|
891
|
+
timeoutId = setTimeout(() => ac.abort(), toolDefForTimeout.timeout);
|
|
892
|
+
context.signal.addEventListener('abort', () => ac.abort(), { once: true });
|
|
893
|
+
effectiveSignal = ac.signal;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 执行工具
|
|
897
|
+
let result: string;
|
|
898
|
+
let toolErrorShape: import('../events').ToolError | undefined;
|
|
899
|
+
let success = true;
|
|
900
|
+
let resultUi = currentTool?.ui;
|
|
901
|
+
let failureReason: ToolFailureReason | undefined;
|
|
902
|
+
try {
|
|
903
|
+
const eventQueue: ChatEvent[] = [];
|
|
904
|
+
let wake: (() => void) | null = null;
|
|
905
|
+
const notify = () => wake?.();
|
|
906
|
+
const pushEvent = (ev: ChatEvent) => { eventQueue.push(ev); notify(); };
|
|
907
|
+
|
|
908
|
+
let toolDone = false;
|
|
909
|
+
let toolValue: import('../types').ToolExecuteResult | undefined;
|
|
910
|
+
let toolError: unknown;
|
|
911
|
+
|
|
912
|
+
const hooks = {
|
|
913
|
+
toolCallId: toolCall.id,
|
|
914
|
+
toolName: toolCall.name,
|
|
915
|
+
allowedToolNames: Array.from(allowedToolNames),
|
|
916
|
+
governance: effectiveGovernance,
|
|
917
|
+
onStdout: (chunk: string) => {
|
|
918
|
+
if (chunk) pushEvent(createToolCallOutput(toolCall.id, toolCall.name, 'stdout', chunk));
|
|
919
|
+
},
|
|
920
|
+
onStderr: (chunk: string) => {
|
|
921
|
+
if (chunk) pushEvent(createToolCallOutput(toolCall.id, toolCall.name, 'stderr', chunk));
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
this.config.executeTool(toolCall.name, args, effectiveSignal, hooks)
|
|
926
|
+
.then((v) => { toolValue = v; toolDone = true; notify(); })
|
|
927
|
+
.catch((e) => { toolError = e; toolDone = true; notify(); });
|
|
928
|
+
|
|
929
|
+
while (!toolDone || eventQueue.length > 0) {
|
|
930
|
+
while (eventQueue.length > 0) {
|
|
931
|
+
const ev = eventQueue.shift();
|
|
932
|
+
if (ev) yield ev;
|
|
933
|
+
}
|
|
934
|
+
if (toolDone) break;
|
|
935
|
+
await new Promise<void>((r) => (wake = r));
|
|
936
|
+
wake = null;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (toolError) throw toolError;
|
|
940
|
+
const normalizedResult = normalizeToolResult(toolValue);
|
|
941
|
+
resultUi = resolveToolResultUi(normalizedResult, currentTool?.ui);
|
|
942
|
+
result = JSON.stringify(serializeToolResult(normalizedResult));
|
|
943
|
+
} catch (error) {
|
|
944
|
+
success = false;
|
|
945
|
+
if (isToolError(error)) {
|
|
946
|
+
toolErrorShape = error.toolError;
|
|
947
|
+
failureReason = error.toolError.code === 'TIMEOUT' ? 'timeout' : 'execution_error';
|
|
948
|
+
result = JSON.stringify(buildToolFailurePayload({
|
|
949
|
+
message: error.toolError.message,
|
|
950
|
+
failureReason,
|
|
951
|
+
code: error.toolError.code,
|
|
952
|
+
category: error.toolError.category ?? inferToolErrorCategory(error.toolError.code, 'runtime'),
|
|
953
|
+
retryable: error.toolError.retryable,
|
|
954
|
+
suggestion: error.toolError.suggestion,
|
|
955
|
+
details: error.toolError.details,
|
|
956
|
+
cause: error.cause ? String(error.cause) : undefined,
|
|
957
|
+
}));
|
|
958
|
+
} else {
|
|
959
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
960
|
+
toolErrorShape = { message: msg };
|
|
961
|
+
failureReason = 'execution_error';
|
|
962
|
+
result = JSON.stringify(buildToolFailurePayload({
|
|
963
|
+
message: msg,
|
|
964
|
+
failureReason,
|
|
965
|
+
category: 'runtime',
|
|
966
|
+
cause: error instanceof Error && error.cause ? String(error.cause) : undefined,
|
|
967
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
968
|
+
}));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
973
|
+
|
|
974
|
+
// 发射工具结果事件
|
|
975
|
+
yield createToolCallResult(
|
|
976
|
+
toolCall.id,
|
|
977
|
+
toolCall.name,
|
|
978
|
+
result,
|
|
979
|
+
success,
|
|
980
|
+
toolStartedAt,
|
|
981
|
+
toolErrorShape,
|
|
982
|
+
resultUi,
|
|
983
|
+
failureReason,
|
|
984
|
+
effectiveGovernance,
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
// 添加 tool 消息到历史
|
|
988
|
+
messages.push({
|
|
989
|
+
role: 'tool',
|
|
990
|
+
content: buildModelToolContent(result, success),
|
|
991
|
+
toolCallId: toolCall.id,
|
|
992
|
+
toolName: toolCall.name,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* 创建 Orchestrator 实例
|
|
999
|
+
*/
|
|
1000
|
+
export function createOrchestrator(config: OrchestratorConfig): ChatOrchestrator {
|
|
1001
|
+
return new ChatOrchestrator(config);
|
|
1002
|
+
}
|