@zhin.js/core 1.0.37 → 1.0.38

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.
Files changed (196) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +57 -3
  3. package/lib/adapter.d.ts +11 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +61 -0
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/index.d.ts +3 -39
  8. package/lib/ai/index.d.ts.map +1 -1
  9. package/lib/ai/index.js +2 -44
  10. package/lib/ai/index.js.map +1 -1
  11. package/lib/ai/types.d.ts +4 -3
  12. package/lib/ai/types.d.ts.map +1 -1
  13. package/lib/built/ai-trigger.js.map +1 -1
  14. package/lib/built/common-adapter-tools.d.ts +55 -0
  15. package/lib/built/common-adapter-tools.d.ts.map +1 -0
  16. package/lib/built/common-adapter-tools.js +158 -0
  17. package/lib/built/common-adapter-tools.js.map +1 -0
  18. package/lib/built/dispatcher.d.ts.map +1 -1
  19. package/lib/built/dispatcher.js +50 -46
  20. package/lib/built/dispatcher.js.map +1 -1
  21. package/lib/built/skill.d.ts.map +1 -1
  22. package/lib/built/skill.js +0 -1
  23. package/lib/built/skill.js.map +1 -1
  24. package/lib/built/tool.d.ts +3 -3
  25. package/lib/built/tool.d.ts.map +1 -1
  26. package/lib/built/tool.js.map +1 -1
  27. package/lib/feature.d.ts +16 -1
  28. package/lib/feature.d.ts.map +1 -1
  29. package/lib/feature.js +41 -2
  30. package/lib/feature.js.map +1 -1
  31. package/lib/index.d.ts +1 -0
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +2 -0
  34. package/lib/index.js.map +1 -1
  35. package/lib/plugin.d.ts +38 -1
  36. package/lib/plugin.d.ts.map +1 -1
  37. package/lib/plugin.js +73 -22
  38. package/lib/plugin.js.map +1 -1
  39. package/lib/scheduler/scheduler.js +1 -1
  40. package/lib/scheduler/scheduler.js.map +1 -1
  41. package/lib/types.d.ts +43 -28
  42. package/lib/types.d.ts.map +1 -1
  43. package/lib/utils.d.ts +12 -3
  44. package/lib/utils.d.ts.map +1 -1
  45. package/lib/utils.js +64 -54
  46. package/lib/utils.js.map +1 -1
  47. package/package.json +5 -5
  48. package/src/adapter.ts +85 -5
  49. package/src/ai/index.ts +8 -186
  50. package/src/ai/types.ts +5 -4
  51. package/src/built/ai-trigger.ts +2 -2
  52. package/src/built/common-adapter-tools.ts +207 -0
  53. package/src/built/dispatcher.ts +51 -52
  54. package/src/built/skill.ts +3 -4
  55. package/src/built/tool.ts +3 -3
  56. package/src/feature.ts +45 -2
  57. package/src/index.ts +2 -0
  58. package/src/plugin.ts +92 -31
  59. package/src/scheduler/scheduler.ts +1 -1
  60. package/src/types.ts +39 -28
  61. package/src/utils.ts +63 -52
  62. package/tests/ai/setup.ts +2 -2
  63. package/tests/utils.test.ts +1 -3
  64. package/lib/ai/agent.d.ts +0 -130
  65. package/lib/ai/agent.d.ts.map +0 -1
  66. package/lib/ai/agent.js +0 -702
  67. package/lib/ai/agent.js.map +0 -1
  68. package/lib/ai/bootstrap.d.ts +0 -91
  69. package/lib/ai/bootstrap.d.ts.map +0 -1
  70. package/lib/ai/bootstrap.js +0 -243
  71. package/lib/ai/bootstrap.js.map +0 -1
  72. package/lib/ai/builtin-tools.d.ts +0 -59
  73. package/lib/ai/builtin-tools.d.ts.map +0 -1
  74. package/lib/ai/builtin-tools.js +0 -777
  75. package/lib/ai/builtin-tools.js.map +0 -1
  76. package/lib/ai/compaction.d.ts +0 -132
  77. package/lib/ai/compaction.d.ts.map +0 -1
  78. package/lib/ai/compaction.js +0 -370
  79. package/lib/ai/compaction.js.map +0 -1
  80. package/lib/ai/context-manager.d.ts +0 -213
  81. package/lib/ai/context-manager.d.ts.map +0 -1
  82. package/lib/ai/context-manager.js +0 -313
  83. package/lib/ai/context-manager.js.map +0 -1
  84. package/lib/ai/conversation-memory.d.ts +0 -181
  85. package/lib/ai/conversation-memory.d.ts.map +0 -1
  86. package/lib/ai/conversation-memory.js +0 -581
  87. package/lib/ai/conversation-memory.js.map +0 -1
  88. package/lib/ai/cron-engine.d.ts +0 -92
  89. package/lib/ai/cron-engine.d.ts.map +0 -1
  90. package/lib/ai/cron-engine.js +0 -278
  91. package/lib/ai/cron-engine.js.map +0 -1
  92. package/lib/ai/follow-up.d.ts +0 -131
  93. package/lib/ai/follow-up.d.ts.map +0 -1
  94. package/lib/ai/follow-up.js +0 -265
  95. package/lib/ai/follow-up.js.map +0 -1
  96. package/lib/ai/hooks.d.ts +0 -143
  97. package/lib/ai/hooks.d.ts.map +0 -1
  98. package/lib/ai/hooks.js +0 -108
  99. package/lib/ai/hooks.js.map +0 -1
  100. package/lib/ai/init.d.ts +0 -30
  101. package/lib/ai/init.d.ts.map +0 -1
  102. package/lib/ai/init.js +0 -686
  103. package/lib/ai/init.js.map +0 -1
  104. package/lib/ai/output.d.ts +0 -93
  105. package/lib/ai/output.d.ts.map +0 -1
  106. package/lib/ai/output.js +0 -176
  107. package/lib/ai/output.js.map +0 -1
  108. package/lib/ai/rate-limiter.d.ts +0 -38
  109. package/lib/ai/rate-limiter.d.ts.map +0 -1
  110. package/lib/ai/rate-limiter.js +0 -86
  111. package/lib/ai/rate-limiter.js.map +0 -1
  112. package/lib/ai/service.d.ts +0 -88
  113. package/lib/ai/service.d.ts.map +0 -1
  114. package/lib/ai/service.js +0 -285
  115. package/lib/ai/service.js.map +0 -1
  116. package/lib/ai/session.d.ts +0 -186
  117. package/lib/ai/session.d.ts.map +0 -1
  118. package/lib/ai/session.js +0 -443
  119. package/lib/ai/session.js.map +0 -1
  120. package/lib/ai/subagent.d.ts +0 -50
  121. package/lib/ai/subagent.d.ts.map +0 -1
  122. package/lib/ai/subagent.js +0 -144
  123. package/lib/ai/subagent.js.map +0 -1
  124. package/lib/ai/tone-detector.d.ts +0 -19
  125. package/lib/ai/tone-detector.d.ts.map +0 -1
  126. package/lib/ai/tone-detector.js +0 -72
  127. package/lib/ai/tone-detector.js.map +0 -1
  128. package/lib/ai/tools.d.ts +0 -45
  129. package/lib/ai/tools.d.ts.map +0 -1
  130. package/lib/ai/tools.js +0 -206
  131. package/lib/ai/tools.js.map +0 -1
  132. package/lib/ai/user-profile.d.ts +0 -56
  133. package/lib/ai/user-profile.d.ts.map +0 -1
  134. package/lib/ai/user-profile.js +0 -130
  135. package/lib/ai/user-profile.js.map +0 -1
  136. package/lib/ai/zhin-agent/builtin-tools.d.ts +0 -17
  137. package/lib/ai/zhin-agent/builtin-tools.d.ts.map +0 -1
  138. package/lib/ai/zhin-agent/builtin-tools.js +0 -220
  139. package/lib/ai/zhin-agent/builtin-tools.js.map +0 -1
  140. package/lib/ai/zhin-agent/config.d.ts +0 -54
  141. package/lib/ai/zhin-agent/config.d.ts.map +0 -1
  142. package/lib/ai/zhin-agent/config.js +0 -76
  143. package/lib/ai/zhin-agent/config.js.map +0 -1
  144. package/lib/ai/zhin-agent/exec-policy.d.ts +0 -20
  145. package/lib/ai/zhin-agent/exec-policy.d.ts.map +0 -1
  146. package/lib/ai/zhin-agent/exec-policy.js +0 -71
  147. package/lib/ai/zhin-agent/exec-policy.js.map +0 -1
  148. package/lib/ai/zhin-agent/index.d.ts +0 -70
  149. package/lib/ai/zhin-agent/index.d.ts.map +0 -1
  150. package/lib/ai/zhin-agent/index.js +0 -404
  151. package/lib/ai/zhin-agent/index.js.map +0 -1
  152. package/lib/ai/zhin-agent/prompt.d.ts +0 -21
  153. package/lib/ai/zhin-agent/prompt.d.ts.map +0 -1
  154. package/lib/ai/zhin-agent/prompt.js +0 -111
  155. package/lib/ai/zhin-agent/prompt.js.map +0 -1
  156. package/lib/ai/zhin-agent/tool-collector.d.ts +0 -22
  157. package/lib/ai/zhin-agent/tool-collector.d.ts.map +0 -1
  158. package/lib/ai/zhin-agent/tool-collector.js +0 -218
  159. package/lib/ai/zhin-agent/tool-collector.js.map +0 -1
  160. package/src/ai/agent.ts +0 -831
  161. package/src/ai/bootstrap.ts +0 -309
  162. package/src/ai/builtin-tools.ts +0 -849
  163. package/src/ai/compaction.ts +0 -529
  164. package/src/ai/context-manager.ts +0 -440
  165. package/src/ai/conversation-memory.ts +0 -774
  166. package/src/ai/cron-engine.ts +0 -337
  167. package/src/ai/follow-up.ts +0 -357
  168. package/src/ai/hooks.ts +0 -223
  169. package/src/ai/init.ts +0 -762
  170. package/src/ai/output.ts +0 -261
  171. package/src/ai/rate-limiter.ts +0 -129
  172. package/src/ai/service.ts +0 -331
  173. package/src/ai/session.ts +0 -544
  174. package/src/ai/subagent.ts +0 -209
  175. package/src/ai/tone-detector.ts +0 -89
  176. package/src/ai/tools.ts +0 -218
  177. package/src/ai/user-profile.ts +0 -181
  178. package/src/ai/zhin-agent/builtin-tools.ts +0 -247
  179. package/src/ai/zhin-agent/config.ts +0 -113
  180. package/src/ai/zhin-agent/exec-policy.ts +0 -78
  181. package/src/ai/zhin-agent/index.ts +0 -512
  182. package/src/ai/zhin-agent/prompt.ts +0 -131
  183. package/src/ai/zhin-agent/tool-collector.ts +0 -243
  184. package/tests/ai/agent.test.ts +0 -614
  185. package/tests/ai/context-manager.test.ts +0 -413
  186. package/tests/ai/conversation-memory.test.ts +0 -128
  187. package/tests/ai/follow-up.test.ts +0 -175
  188. package/tests/ai/integration.test.ts +0 -584
  189. package/tests/ai/output.test.ts +0 -128
  190. package/tests/ai/rate-limiter.test.ts +0 -108
  191. package/tests/ai/session.test.ts +0 -375
  192. package/tests/ai/subagent.test.ts +0 -270
  193. package/tests/ai/tone-detector.test.ts +0 -80
  194. package/tests/ai/tools-builtin.test.ts +0 -346
  195. package/tests/ai/user-profile.test.ts +0 -73
  196. package/tests/ai/zhin-agent.test.ts +0 -177
package/src/ai/agent.ts DELETED
@@ -1,831 +0,0 @@
1
- /**
2
- * @zhin.js/ai - Agent System
3
- * AI Agent 实现,支持工具调用和多轮对话
4
- */
5
-
6
- import { Logger } from '@zhin.js/logger';
7
- import type {
8
- AIProvider,
9
- AgentConfig,
10
- AgentTool,
11
- AgentResult,
12
- ChatMessage,
13
- ToolDefinition,
14
- ToolCall,
15
- ToolFilterOptions,
16
- Usage,
17
- } from './types.js';
18
-
19
- const logger = new Logger(null, 'Agent');
20
-
21
- /** 工具执行默认超时时间 (ms) */
22
- const DEFAULT_TOOL_TIMEOUT = 30_000;
23
-
24
- /**
25
- * 根据工具名和参数生成简短标题(用于日志、TOOLS.md 等)
26
- */
27
- export function formatToolTitle(name: string, args?: Record<string, any>): string {
28
- if (!args || Object.keys(args).length === 0) return name;
29
- const a = args;
30
- switch (name) {
31
- case 'bash': return a.command != null ? `bash: ${String(a.command).slice(0, 60)}` : name;
32
- case 'read_file': return a.file_path != null ? `read_file: ${a.file_path}` : name;
33
- case 'write_file': return a.file_path != null ? `write_file: ${a.file_path}` : name;
34
- case 'edit_file': return a.file_path != null ? `edit_file: ${a.file_path}` : name;
35
- case 'list_dir': return a.path != null ? `list_dir: ${a.path}` : name;
36
- case 'web_search': return a.query != null ? `web_search: ${String(a.query).slice(0, 40)}` : name;
37
- case 'web_fetch': return a.url != null ? `web_fetch: ${String(a.url).slice(0, 50)}` : name;
38
- default: {
39
- const first = Object.values(a)[0];
40
- if (first != null) return `${name}: ${String(first).slice(0, 50)}`;
41
- return name;
42
- }
43
- }
44
- }
45
-
46
- /**
47
- * Agent 执行状态
48
- */
49
- export interface AgentState {
50
- messages: ChatMessage[];
51
- toolCalls: { tool: string; args: Record<string, any>; result: any }[];
52
- usage: Usage;
53
- iterations: number;
54
- }
55
-
56
- /**
57
- * Agent 事件
58
- */
59
- export interface AgentEvents {
60
- 'thinking': (message: string) => void;
61
- 'tool_call': (tool: string, args: Record<string, any>) => void;
62
- 'tool_result': (tool: string, result: any) => void;
63
- 'streaming': (content: string) => void;
64
- 'complete': (result: AgentResult) => void;
65
- 'error': (error: Error) => void;
66
- }
67
-
68
- /**
69
- * AI Agent 类
70
- * 支持工具调用、多轮对话、流式输出
71
- */
72
- export class Agent {
73
- private provider: AIProvider;
74
- private config: Required<AgentConfig>;
75
- private tools: Map<string, AgentTool> = new Map();
76
- private eventHandlers: Map<keyof AgentEvents, Function[]> = new Map();
77
-
78
- constructor(provider: AIProvider, config: AgentConfig) {
79
- this.provider = provider;
80
- this.config = {
81
- provider: config.provider,
82
- model: config.model || provider.models[0],
83
- systemPrompt: config.systemPrompt || this.getDefaultSystemPrompt(),
84
- tools: config.tools || [],
85
- maxIterations: config.maxIterations || 10,
86
- temperature: config.temperature ?? 0.7,
87
- };
88
-
89
- // 注册工具
90
- for (const tool of this.config.tools) {
91
- this.tools.set(tool.name, tool);
92
- }
93
- }
94
-
95
- /**
96
- * 默认系统提示词
97
- */
98
- private getDefaultSystemPrompt(): string {
99
- return `你是一个智能助手,可以使用工具来帮助用户完成任务。
100
- 请遵循以下原则:
101
- 1. 理解用户的意图,选择合适的工具
102
- 2. 如果需要多个步骤,逐步执行
103
- 3. 清晰地解释你的行动和结果
104
- 4. 如果无法完成任务,诚实地告知用户`;
105
- }
106
-
107
- /**
108
- * 注册事件处理器
109
- */
110
- on<K extends keyof AgentEvents>(event: K, handler: AgentEvents[K]): () => void {
111
- const handlers = this.eventHandlers.get(event) || [];
112
- handlers.push(handler);
113
- this.eventHandlers.set(event, handlers);
114
-
115
- return () => {
116
- const idx = handlers.indexOf(handler);
117
- if (idx !== -1) handlers.splice(idx, 1);
118
- };
119
- }
120
-
121
- /**
122
- * 触发事件
123
- */
124
- private emit<K extends keyof AgentEvents>(event: K, ...args: Parameters<AgentEvents[K]>): void {
125
- const handlers = this.eventHandlers.get(event) || [];
126
- for (const handler of handlers) {
127
- try {
128
- (handler as Function)(...args);
129
- } catch (e) {
130
- logger.error('事件处理器错误:', e);
131
- }
132
- }
133
- }
134
-
135
- /**
136
- * 添加工具
137
- */
138
- addTool(tool: AgentTool): void {
139
- this.tools.set(tool.name, tool);
140
- }
141
-
142
- /**
143
- * 移除工具
144
- */
145
- removeTool(name: string): void {
146
- this.tools.delete(name);
147
- }
148
-
149
- /**
150
- * 获取工具定义(缓存在第一次调用后保持不变)
151
- */
152
- private getToolDefinitions(): ToolDefinition[] {
153
- return Array.from(this.tools.values()).map(tool => ({
154
- type: 'function',
155
- function: {
156
- name: tool.name,
157
- description: tool.description,
158
- parameters: tool.parameters,
159
- },
160
- }));
161
- }
162
-
163
- /**
164
- * 生成工具调用的去重 key(规范化参数以避免 "" vs "{}" 等差异)
165
- */
166
- private static toolCallKey(name: string, args: string): string {
167
- let normalized: string;
168
- try {
169
- const parsed = JSON.parse(args || '{}');
170
- normalized = JSON.stringify(parsed, Object.keys(parsed).sort());
171
- } catch {
172
- normalized = args || '';
173
- }
174
- return `${name}::${normalized}`;
175
- }
176
-
177
- /**
178
- * 安全解析 JSON,失败则返回原始字符串
179
- */
180
- private static safeParse(str: string): any {
181
- try {
182
- return JSON.parse(str);
183
- } catch {
184
- return str;
185
- }
186
- }
187
-
188
- /**
189
- * 程序化工具过滤 —— 替代 AI 意图分析的高效方案
190
- *
191
- * 评分规则(按权重从高到低):
192
- * 1. keywords 精确匹配: +1.0 per hit —— 工具声明的触发关键词
193
- * 2. tags 匹配: +0.5 per hit —— 工具分类标签
194
- * 3. 工具名 token 匹配: +0.3 per hit —— 工具名按 `.` `_` `-` 拆词
195
- * 4. description 关键词: +0.15 per hit —— 描述中的词/短语
196
- *
197
- * 权限过滤发生在评分之前,直接跳过无权使用的工具。
198
- *
199
- * @param message 用户消息原文
200
- * @param tools 候选工具列表
201
- * @param options 过滤选项
202
- * @returns 按相关性降序排列的工具子集
203
- */
204
- static filterTools(
205
- message: string,
206
- tools: AgentTool[],
207
- options?: ToolFilterOptions,
208
- ): AgentTool[] {
209
- if (tools.length === 0) return [];
210
-
211
- const maxTools = options?.maxTools ?? 10;
212
- const minScore = options?.minScore ?? 0.1;
213
- const callerPerm = options?.callerPermissionLevel ?? Infinity;
214
-
215
- const msgLower = message.toLowerCase();
216
-
217
- const scored: { tool: AgentTool; score: number }[] = [];
218
-
219
- for (const tool of tools) {
220
- // ── 权限过滤 ──
221
- if (tool.permissionLevel != null && tool.permissionLevel > callerPerm) {
222
- continue;
223
- }
224
-
225
- let score = 0;
226
-
227
- // ── 1. keywords 匹配(最高权重) ──
228
- if (tool.keywords?.length) {
229
- for (const kw of tool.keywords) {
230
- if (kw && msgLower.includes(kw.toLowerCase())) {
231
- score += 1.0;
232
- }
233
- }
234
- }
235
-
236
- // ── 2. tags 匹配 ──
237
- if (tool.tags?.length) {
238
- for (const tag of tool.tags) {
239
- if (tag && tag.length > 1 && msgLower.includes(tag.toLowerCase())) {
240
- score += 0.5;
241
- }
242
- }
243
- }
244
-
245
- // ── 3. 工具名 token 匹配 ──
246
- const nameTokens = tool.name.toLowerCase().split(/[._\-]+/);
247
- for (const nt of nameTokens) {
248
- if (nt.length > 1 && msgLower.includes(nt)) {
249
- score += 0.3;
250
- }
251
- }
252
-
253
- // ── 4. 描述双向匹配(对中文友好) ──
254
- // 4a. 描述词 → 出现在用户消息中?
255
- const descLower = tool.description.toLowerCase();
256
- const descWords = descLower
257
- .split(/[\s,.:;!?,。:;!?、()()【】\[\]]+/)
258
- .filter(w => w.length >= 2);
259
- for (const dw of descWords) {
260
- if (msgLower.includes(dw)) {
261
- score += 0.15;
262
- }
263
- }
264
- // 4b. 用户消息词 → 出现在描述中?(中文无空格分词,逆向补偿)
265
- const msgWords = msgLower
266
- .split(/[\s,.:;!?,。:;!?、()()【】\[\]]+/)
267
- .filter(w => w.length >= 2);
268
- for (const mw of msgWords) {
269
- if (descLower.includes(mw)) {
270
- score += 0.2;
271
- }
272
- }
273
-
274
- if (score >= minScore) {
275
- scored.push({ tool, score });
276
- }
277
- }
278
-
279
- // 按分数降序
280
- scored.sort((a, b) => b.score - a.score);
281
-
282
- return scored.slice(0, maxTools).map(s => s.tool);
283
- }
284
-
285
- /**
286
- * 执行单个工具调用(带超时保护)
287
- */
288
- private async executeToolCall(toolCall: ToolCall): Promise<string> {
289
- const tool = this.tools.get(toolCall.function.name);
290
- if (!tool) {
291
- return JSON.stringify({
292
- error: `Unknown tool: ${toolCall.function.name}`,
293
- hint: '该工具不存在,请尝试使用其他可用工具,或直接回答用户。',
294
- });
295
- }
296
-
297
- let args: Record<string, unknown>;
298
- try {
299
- args = JSON.parse(toolCall.function.arguments);
300
- } catch {
301
- return JSON.stringify({
302
- error: 'Invalid tool arguments JSON',
303
- tool: toolCall.function.name,
304
- hint: '请检查工具参数格式后重试。',
305
- });
306
- }
307
-
308
- logger.debug({ tool: toolCall.function.name, params: args }, 'Executing tool');
309
- this.emit('tool_call', tool.name, args);
310
-
311
- try {
312
- // 带超时的工具执行
313
- const result = await Promise.race([
314
- tool.execute(args),
315
- new Promise<never>((_, reject) =>
316
- setTimeout(() => reject(new Error(`工具 ${tool.name} 执行超时`)), DEFAULT_TOOL_TIMEOUT),
317
- ),
318
- ]);
319
-
320
- this.emit('tool_result', tool.name, result);
321
- return typeof result === 'string' ? result : JSON.stringify(result);
322
- } catch (error) {
323
- const errorMsg = error instanceof Error ? error.message : String(error);
324
- logger.warn(`工具 ${toolCall.function.name} 执行失败: ${errorMsg}`);
325
- logger.error({ tool: toolCall.function.name, params: args, err: error }, 'Tool execution failed');
326
- // 向 AI 提供结构化的错误信息和恢复提示
327
- return JSON.stringify({
328
- error: errorMsg,
329
- tool: toolCall.function.name,
330
- hint: '该工具执行失败。请尝试使用不同的参数重试,或换一个工具来完成任务。如果所有工具都无法使用,请直接用文字回答用户。',
331
- });
332
- }
333
- }
334
-
335
- /**
336
- * 并行执行多个工具调用(跳过重复的)
337
- * @returns 新执行的工具调用结果列表;如果全部重复则返回空数组
338
- */
339
- private async executeToolCalls(
340
- toolCalls: ToolCall[],
341
- seenKeys: Set<string>,
342
- state: AgentState,
343
- ): Promise<{ toolCall: ToolCall; result: string; args: Record<string, any> }[]> {
344
- // 分离:新调用 vs 重复调用
345
- const fresh: ToolCall[] = [];
346
- for (const tc of toolCalls) {
347
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
348
- if (seenKeys.has(key)) {
349
- logger.debug(`跳过重复工具调用: ${tc.function.name}`);
350
- } else {
351
- fresh.push(tc);
352
- }
353
- }
354
-
355
- if (fresh.length === 0) return [];
356
-
357
- // 并行执行所有新工具调用
358
- const tasks = fresh.map(async (tc) => {
359
- const result = await this.executeToolCall(tc);
360
- const args = Agent.safeParse(tc.function.arguments);
361
- const parsedResult = Agent.safeParse(result);
362
-
363
- // 记录到状态
364
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
365
- seenKeys.add(key);
366
- state.toolCalls.push({
367
- tool: tc.function.name,
368
- args: typeof args === 'object' ? args : { raw: args },
369
- result: parsedResult,
370
- });
371
-
372
- return { toolCall: tc, result, args };
373
- });
374
-
375
- return Promise.all(tasks);
376
- }
377
-
378
- /**
379
- * 累加 token 用量
380
- */
381
- private static addUsage(target: Usage, source?: Usage): void {
382
- if (!source) return;
383
- target.prompt_tokens += source.prompt_tokens;
384
- target.completion_tokens += source.completion_tokens;
385
- target.total_tokens += source.total_tokens;
386
- }
387
-
388
- /**
389
- * 运行 Agent
390
- *
391
- * @param userMessage 用户消息
392
- * @param context 对话上下文
393
- * @param filterOptions 工具过滤选项 —— 启用后在 AI 调用之前程序化筛选工具,省去额外的 AI 意图分析往返
394
- */
395
- async run(userMessage: string, context?: ChatMessage[], filterOptions?: ToolFilterOptions): Promise<AgentResult> {
396
- const state: AgentState = {
397
- messages: [
398
- { role: 'system', content: this.config.systemPrompt },
399
- ...(context || []),
400
- { role: 'user', content: userMessage },
401
- ],
402
- toolCalls: [],
403
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
404
- iterations: 0,
405
- };
406
-
407
- // 程序化工具预过滤:只把相关工具传给 AI,减少 token 消耗和误选
408
- let toolDefinitions: ToolDefinition[];
409
- if (filterOptions) {
410
- const allTools = Array.from(this.tools.values());
411
- const filtered = Agent.filterTools(userMessage, allTools, filterOptions);
412
- logger.info(`工具预过滤: ${allTools.length} -> ${filtered.length}`);
413
- toolDefinitions = filtered.map(tool => ({
414
- type: 'function' as const,
415
- function: {
416
- name: tool.name,
417
- description: tool.description,
418
- parameters: tool.parameters,
419
- },
420
- }));
421
- } else {
422
- toolDefinitions = this.getToolDefinitions();
423
- }
424
- const hasTools = toolDefinitions.length > 0;
425
- // O(1) 去重集合
426
- const seenToolKeys = new Set<string>();
427
- // 连续全重复计数器
428
- let consecutiveDuplicateRounds = 0;
429
-
430
- while (state.iterations < this.config.maxIterations) {
431
- state.iterations++;
432
-
433
- // 强制文本回答的条件:
434
- // 1. 检测到连续重复工具调用
435
- // 2. 最后一轮迭代且已有工具结果 —— 保证 Agent 始终输出文本,不再需要额外的 summary 往返
436
- const isLastIteration = state.iterations >= this.config.maxIterations;
437
- const forceAnswer = consecutiveDuplicateRounds > 0 ||
438
- (isLastIteration && state.toolCalls.length > 0);
439
-
440
- try {
441
- // 工具调用轮次禁用思考(qwen3 等模型),大幅减少无效 token 生成
442
- const isToolCallRound = hasTools && !forceAnswer;
443
- const response = await this.provider.chat({
444
- model: this.config.model,
445
- messages: state.messages,
446
- tools: isToolCallRound ? toolDefinitions : undefined,
447
- tool_choice: isToolCallRound ? 'auto' : undefined,
448
- temperature: this.config.temperature,
449
- think: isToolCallRound ? false : undefined,
450
- });
451
-
452
- Agent.addUsage(state.usage, response.usage);
453
- logger.info(`token 用量: ${state.usage.prompt_tokens} -> ${state.usage.completion_tokens} -> ${state.usage.total_tokens}`);
454
- logger.info(`response: `,response);
455
- const choice = response.choices[0];
456
- if (!choice) break;
457
-
458
- // ── 分支 1: 模型想调用工具 ──
459
- if (choice.message.tool_calls?.length) {
460
- const callSummary = choice.message.tool_calls.map(
461
- (tc: any) => `${tc.function.name}(${tc.function.arguments})`
462
- ).join(', ');
463
- logger.info(`[第${state.iterations}轮] 工具调用: ${callSummary}`);
464
- this.emit('thinking', '正在执行工具调用...');
465
-
466
- // 当存在 tool_calls 时,content 通常是模型的内部思考或原始 JSON,
467
- // 不需要暴露给最终用户,但需要保留在消息历史中以维持对话完整性
468
- state.messages.push({
469
- role: 'tool_call',
470
- content: typeof choice.message.content === 'string' ? choice.message.content : '',
471
- tool_calls: choice.message.tool_calls,
472
- });
473
-
474
- // 并行执行所有新工具调用,自动跳过重复
475
- const results = await this.executeToolCalls(
476
- choice.message.tool_calls,
477
- seenToolKeys,
478
- state,
479
- );
480
-
481
- if (results.length === 0) {
482
- consecutiveDuplicateRounds++;
483
- logger.warn(`[第${state.iterations}轮] 检测到重复工具调用,已跳过执行,强制下轮文本回答`);
484
-
485
- for (const tc of choice.message.tool_calls) {
486
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
487
- const previous = state.toolCalls.find(
488
- stc => Agent.toolCallKey(stc.tool, JSON.stringify(stc.args)) === key ||
489
- Agent.toolCallKey(stc.tool, tc.function.arguments) === key,
490
- );
491
- state.messages.push({
492
- role: 'tool',
493
- content: previous ? JSON.stringify(previous.result) : '结果已获取',
494
- tool_call_id: tc.id,
495
- });
496
- }
497
-
498
- state.messages.push({
499
- role: 'system',
500
- content: '你已经获取了所需的全部信息,请直接用自然语言回答用户,不要再调用工具。',
501
- });
502
-
503
- continue;
504
- }
505
-
506
- // 有新的工具调用被执行
507
- consecutiveDuplicateRounds = 0;
508
-
509
- // 将工具结果加入消息历史
510
- for (const { toolCall, result } of results) {
511
- const resultPreview = result.length > 200 ? result.slice(0, 200) + '...' : result;
512
- logger.info(`[第${state.iterations}轮] 工具结果 ${toolCall.function.name}: ${resultPreview}`);
513
- state.messages.push({
514
- role: 'tool',
515
- content: result,
516
- tool_call_id: toolCall.id,
517
- });
518
- }
519
-
520
- // 如果工具返回的是最终结果(非查询中间步骤),引导模型直接回复
521
- const allSucceeded = results.every(r => !r.result.startsWith('{'));
522
- if (allSucceeded && results.length > 0) {
523
- state.messages.push({
524
- role: 'system',
525
- content: '工具已返回结果。如果信息足够回答用户问题,请直接用自然语言回答,不要重复调用相同工具。',
526
- });
527
- }
528
-
529
- continue;
530
- }
531
-
532
- // ── 分支 2: 模型返回文本回答 ──
533
- const content = typeof choice.message.content === 'string'
534
- ? choice.message.content
535
- : '';
536
-
537
- const result: AgentResult = {
538
- content,
539
- toolCalls: state.toolCalls,
540
- usage: state.usage,
541
- iterations: state.iterations,
542
- };
543
-
544
- this.emit('complete', result);
545
- return result;
546
-
547
- } catch (error) {
548
- const err = error instanceof Error ? error : new Error(String(error));
549
- this.emit('error', err);
550
-
551
- // ── 错误恢复策略 ──
552
- // 如果已经有工具结果,注入恢复消息让 AI 基于已有数据回答
553
- if (state.toolCalls.length > 0) {
554
- logger.warn(`第 ${state.iterations} 轮 LLM 调用失败,尝试基于已有数据恢复: ${err.message}`);
555
- const toolSummary = state.toolCalls.map(tc => {
556
- const r = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result);
557
- return `【${tc.tool}】${r}`;
558
- }).join('\n');
559
- const fallbackResult: AgentResult = {
560
- content: `以下是已获取的工具结果:\n${toolSummary}`,
561
- toolCalls: state.toolCalls,
562
- usage: state.usage,
563
- iterations: state.iterations,
564
- };
565
- this.emit('complete', fallbackResult);
566
- return fallbackResult;
567
- }
568
-
569
- // 没有任何工具结果,提供友好的错误消息
570
- const fallbackResult: AgentResult = {
571
- content: `抱歉,处理过程中遇到了问题:${err.message}。请稍后重试或换个方式提问。`,
572
- toolCalls: [],
573
- usage: state.usage,
574
- iterations: state.iterations,
575
- };
576
- this.emit('complete', fallbackResult);
577
- return fallbackResult;
578
- }
579
- }
580
-
581
- // 达到最大迭代次数,基于已有工具结果生成兜底回复
582
- let fallbackContent: string;
583
- if (state.toolCalls.length > 0) {
584
- // 尝试从工具结果中构建有意义的回复
585
- const toolSummary = state.toolCalls.map(tc => {
586
- const r = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result);
587
- return `【${tc.tool}】${r}`;
588
- }).join('\n');
589
- fallbackContent = `处理完成,以下是获取到的信息:\n${toolSummary}`;
590
- } else {
591
- fallbackContent = '达到最大处理轮次,任务可能未完全完成。请尝试简化问题后重试。';
592
- }
593
-
594
- const result: AgentResult = {
595
- content: fallbackContent,
596
- toolCalls: state.toolCalls,
597
- usage: state.usage,
598
- iterations: state.iterations,
599
- };
600
-
601
- this.emit('complete', result);
602
- return result;
603
- }
604
-
605
- /**
606
- * 流式运行 Agent
607
- *
608
- * @param userMessage 用户消息
609
- * @param context 对话上下文
610
- * @param filterOptions 工具过滤选项 —— 启用后在 AI 调用之前程序化筛选工具
611
- */
612
- async *runStream(userMessage: string, context?: ChatMessage[], filterOptions?: ToolFilterOptions): AsyncIterable<{
613
- type: 'content' | 'tool_call' | 'tool_result' | 'done';
614
- data: any;
615
- }> {
616
- const messages: ChatMessage[] = [
617
- { role: 'system', content: this.config.systemPrompt },
618
- ...(context || []),
619
- { role: 'user', content: userMessage },
620
- ];
621
-
622
- // 程序化工具预过滤
623
- let toolDefinitions: ToolDefinition[];
624
- if (filterOptions) {
625
- const allTools = Array.from(this.tools.values());
626
- const filtered = Agent.filterTools(userMessage, allTools, filterOptions);
627
- logger.debug(`流式工具预过滤: ${allTools.length} -> ${filtered.length}`);
628
- toolDefinitions = filtered.map(tool => ({
629
- type: 'function' as const,
630
- function: {
631
- name: tool.name,
632
- description: tool.description,
633
- parameters: tool.parameters,
634
- },
635
- }));
636
- } else {
637
- toolDefinitions = this.getToolDefinitions();
638
- }
639
- const hasTools = toolDefinitions.length > 0;
640
- let iterations = 0;
641
- const toolCallHistory: { tool: string; args: any; result: any }[] = [];
642
- const usage: Usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
643
- const seenToolKeys = new Set<string>();
644
- let consecutiveDuplicateRounds = 0;
645
-
646
- while (iterations < this.config.maxIterations) {
647
- iterations++;
648
-
649
- let content = '';
650
- const pendingToolCalls: ToolCall[] = [];
651
- let finishReason: string | null = null;
652
- const isLastIteration = iterations >= this.config.maxIterations;
653
- const forceAnswer = consecutiveDuplicateRounds > 0 ||
654
- (isLastIteration && toolCallHistory.length > 0);
655
-
656
- // 流式获取响应
657
- for await (const chunk of this.provider.chatStream({
658
- model: this.config.model,
659
- messages,
660
- tools: hasTools && !forceAnswer ? toolDefinitions : undefined,
661
- tool_choice: hasTools && !forceAnswer ? 'auto' : undefined,
662
- temperature: this.config.temperature,
663
- })) {
664
- const choice = chunk.choices[0];
665
- if (!choice) continue;
666
-
667
- // 处理内容片段
668
- if (choice.delta.content) {
669
- content += choice.delta.content;
670
- // 仅在非工具调用阶段输出内容给消费者
671
- if (pendingToolCalls.length === 0) {
672
- yield { type: 'content', data: choice.delta.content };
673
- }
674
- }
675
-
676
- // 合并工具调用片段
677
- if (choice.delta.tool_calls) {
678
- for (const tc of choice.delta.tool_calls) {
679
- let existing = pendingToolCalls.find(p => p.id === tc.id);
680
- if (!existing && tc.id) {
681
- existing = {
682
- id: tc.id,
683
- type: 'function',
684
- function: { name: '', arguments: '' },
685
- };
686
- pendingToolCalls.push(existing);
687
- }
688
- if (existing && tc.function) {
689
- if (tc.function.name) existing.function.name += tc.function.name;
690
- if (tc.function.arguments) existing.function.arguments += tc.function.arguments;
691
- }
692
- }
693
- }
694
-
695
- if (choice.finish_reason) finishReason = choice.finish_reason;
696
-
697
- Agent.addUsage(usage, chunk.usage);
698
- }
699
-
700
- // 将 assistant 消息加入上下文
701
- messages.push({
702
- role: 'tool_call',
703
- content,
704
- tool_calls: pendingToolCalls.length > 0 ? pendingToolCalls : undefined,
705
- });
706
-
707
- // 处理工具调用
708
- if (pendingToolCalls.length > 0 && finishReason === 'tool_calls') {
709
- // 分离新调用和重复调用
710
- const freshCalls: ToolCall[] = [];
711
- const duplicateCalls: ToolCall[] = [];
712
-
713
- for (const tc of pendingToolCalls) {
714
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
715
- if (seenToolKeys.has(key)) {
716
- duplicateCalls.push(tc);
717
- } else {
718
- freshCalls.push(tc);
719
- }
720
- }
721
-
722
- if (freshCalls.length === 0) {
723
- // 全部重复
724
- consecutiveDuplicateRounds++;
725
-
726
- // 补上 tool 消息保持协议完整
727
- for (const tc of pendingToolCalls) {
728
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
729
- const previous = toolCallHistory.find(
730
- h => Agent.toolCallKey(h.tool, JSON.stringify(h.args)) === key ||
731
- Agent.toolCallKey(h.tool, tc.function.arguments) === key,
732
- );
733
- messages.push({
734
- role: 'tool_result',
735
- content: previous ? JSON.stringify(previous.result) : '结果已获取',
736
- tool_call_id: tc.id,
737
- });
738
- }
739
- continue;
740
- }
741
-
742
- consecutiveDuplicateRounds = 0;
743
-
744
- // 先通知上层所有工具调用开始
745
- for (const tc of freshCalls) {
746
- yield { type: 'tool_call', data: { name: tc.function.name, args: tc.function.arguments } };
747
- }
748
-
749
- // 并行执行所有新工具调用
750
- const results = await Promise.all(
751
- freshCalls.map(async (toolCall) => {
752
- const result = await this.executeToolCall(toolCall);
753
- const args = Agent.safeParse(toolCall.function.arguments);
754
- const parsedResult = Agent.safeParse(result);
755
-
756
- const key = Agent.toolCallKey(toolCall.function.name, toolCall.function.arguments);
757
- seenToolKeys.add(key);
758
- toolCallHistory.push({
759
- tool: toolCall.function.name,
760
- args: typeof args === 'object' ? args : { raw: args },
761
- result: parsedResult,
762
- });
763
-
764
- return { toolCall, result };
765
- }),
766
- );
767
-
768
- // yield 工具结果并加入消息历史
769
- for (const { toolCall, result } of results) {
770
- yield { type: 'tool_result', data: { name: toolCall.function.name, result } };
771
- messages.push({
772
- role: 'tool',
773
- content: result,
774
- tool_call_id: toolCall.id,
775
- });
776
- }
777
-
778
- // 为重复的调用也补上 tool 消息
779
- for (const tc of duplicateCalls) {
780
- const key = Agent.toolCallKey(tc.function.name, tc.function.arguments);
781
- const previous = toolCallHistory.find(
782
- h => Agent.toolCallKey(h.tool, JSON.stringify(h.args)) === key ||
783
- Agent.toolCallKey(h.tool, tc.function.arguments) === key,
784
- );
785
- messages.push({
786
- role: 'tool',
787
- content: previous ? JSON.stringify(previous.result) : '结果已获取',
788
- tool_call_id: tc.id,
789
- });
790
- }
791
-
792
- continue;
793
- }
794
-
795
- // 完成
796
- yield {
797
- type: 'done',
798
- data: {
799
- content,
800
- toolCalls: toolCallHistory,
801
- usage,
802
- iterations,
803
- },
804
- };
805
- return;
806
- }
807
-
808
- // 达到最大迭代次数
809
- yield {
810
- type: 'done',
811
- data: {
812
- content: toolCallHistory.length > 0
813
- ? `处理完成,共执行了 ${toolCallHistory.length} 个工具调用。`
814
- : '达到最大迭代次数',
815
- toolCalls: toolCallHistory,
816
- usage,
817
- iterations,
818
- },
819
- };
820
- }
821
- }
822
-
823
- /**
824
- * 创建 Agent 实例
825
- */
826
- export function createAgent(provider: AIProvider, config: Omit<AgentConfig, 'provider'>): Agent {
827
- return new Agent(provider, {
828
- ...config,
829
- provider: provider.name,
830
- });
831
- }