@zhin.js/agent 0.1.20 → 0.1.22

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 (143) hide show
  1. package/README.md +37 -23
  2. package/lib/agent-prompt/format.d.ts +6 -0
  3. package/lib/agent-prompt/format.d.ts.map +1 -0
  4. package/lib/agent-prompt/format.js +40 -0
  5. package/lib/agent-prompt/format.js.map +1 -0
  6. package/lib/agent-prompt/index.d.ts +5 -0
  7. package/lib/agent-prompt/index.d.ts.map +1 -0
  8. package/lib/agent-prompt/index.js +4 -0
  9. package/lib/agent-prompt/index.js.map +1 -0
  10. package/lib/agent-prompt/registry.d.ts +7 -0
  11. package/lib/agent-prompt/registry.d.ts.map +1 -0
  12. package/lib/agent-prompt/registry.js +23 -0
  13. package/lib/agent-prompt/registry.js.map +1 -0
  14. package/lib/agent-prompt/resolver.d.ts +12 -0
  15. package/lib/agent-prompt/resolver.d.ts.map +1 -0
  16. package/lib/agent-prompt/resolver.js +71 -0
  17. package/lib/agent-prompt/resolver.js.map +1 -0
  18. package/lib/builtin/run-deferred-task-tool.d.ts +20 -0
  19. package/lib/builtin/run-deferred-task-tool.d.ts.map +1 -0
  20. package/lib/builtin/run-deferred-task-tool.js +39 -0
  21. package/lib/builtin/run-deferred-task-tool.js.map +1 -0
  22. package/lib/builtin/tool-search-tool.d.ts +19 -0
  23. package/lib/builtin/tool-search-tool.d.ts.map +1 -0
  24. package/lib/builtin/tool-search-tool.js +54 -0
  25. package/lib/builtin/tool-search-tool.js.map +1 -0
  26. package/lib/builtin-tools.d.ts +1 -1
  27. package/lib/builtin-tools.d.ts.map +1 -1
  28. package/lib/builtin-tools.js +1 -5
  29. package/lib/builtin-tools.js.map +1 -1
  30. package/lib/cron-engine.d.ts.map +1 -1
  31. package/lib/cron-engine.js +3 -3
  32. package/lib/cron-engine.js.map +1 -1
  33. package/lib/deferred-worker-runner.d.ts +30 -0
  34. package/lib/deferred-worker-runner.d.ts.map +1 -0
  35. package/lib/deferred-worker-runner.js +159 -0
  36. package/lib/deferred-worker-runner.js.map +1 -0
  37. package/lib/deferred-worker-tool-load.d.ts +6 -0
  38. package/lib/deferred-worker-tool-load.d.ts.map +1 -0
  39. package/lib/deferred-worker-tool-load.js +5 -0
  40. package/lib/deferred-worker-tool-load.js.map +1 -0
  41. package/lib/hooks.d.ts +13 -0
  42. package/lib/hooks.d.ts.map +1 -1
  43. package/lib/hooks.js.map +1 -1
  44. package/lib/index.d.ts +3 -3
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +2 -3
  47. package/lib/index.js.map +1 -1
  48. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  49. package/lib/init/create-zhin-agent.js +9 -7
  50. package/lib/init/create-zhin-agent.js.map +1 -1
  51. package/lib/init/register-ai-service.d.ts.map +1 -1
  52. package/lib/init/register-ai-service.js +5 -5
  53. package/lib/init/register-ai-service.js.map +1 -1
  54. package/lib/init/register-ai-trigger.d.ts.map +1 -1
  55. package/lib/init/register-ai-trigger.js +25 -9
  56. package/lib/init/register-ai-trigger.js.map +1 -1
  57. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  58. package/lib/init/register-builtin-tools.js +33 -29
  59. package/lib/init/register-builtin-tools.js.map +1 -1
  60. package/lib/init/register-mcp-from-config.d.ts +2 -0
  61. package/lib/init/register-mcp-from-config.d.ts.map +1 -0
  62. package/lib/init/register-mcp-from-config.js +85 -0
  63. package/lib/init/register-mcp-from-config.js.map +1 -0
  64. package/lib/init/register-mcp-memory-default.d.ts +2 -0
  65. package/lib/init/register-mcp-memory-default.d.ts.map +1 -0
  66. package/lib/init/register-mcp-memory-default.js +34 -0
  67. package/lib/init/register-mcp-memory-default.js.map +1 -0
  68. package/lib/init.d.ts.map +1 -1
  69. package/lib/init.js +4 -0
  70. package/lib/init.js.map +1 -1
  71. package/lib/mcp-client/index.d.ts +1 -2
  72. package/lib/mcp-client/index.d.ts.map +1 -1
  73. package/lib/mcp-client/index.js +1 -2
  74. package/lib/mcp-client/index.js.map +1 -1
  75. package/lib/orchestrator/mcp-lifecycle.d.ts +6 -0
  76. package/lib/orchestrator/mcp-lifecycle.d.ts.map +1 -0
  77. package/lib/orchestrator/mcp-lifecycle.js +7 -0
  78. package/lib/orchestrator/mcp-lifecycle.js.map +1 -0
  79. package/lib/orchestrator/mcp-registry.d.ts +5 -1
  80. package/lib/orchestrator/mcp-registry.d.ts.map +1 -1
  81. package/lib/orchestrator/mcp-registry.js +36 -20
  82. package/lib/orchestrator/mcp-registry.js.map +1 -1
  83. package/lib/orchestrator/owner-confirm-orchestration.d.ts +4 -1
  84. package/lib/orchestrator/owner-confirm-orchestration.d.ts.map +1 -1
  85. package/lib/orchestrator/owner-confirm-orchestration.js.map +1 -1
  86. package/lib/orchestrator/tool-selection.d.ts +2 -0
  87. package/lib/orchestrator/tool-selection.d.ts.map +1 -1
  88. package/lib/orchestrator/tool-selection.js +40 -4
  89. package/lib/orchestrator/tool-selection.js.map +1 -1
  90. package/lib/reserved-tools.js +2 -2
  91. package/lib/reserved-tools.js.map +1 -1
  92. package/lib/security/bash-tool-context.d.ts +17 -2
  93. package/lib/security/bash-tool-context.d.ts.map +1 -1
  94. package/lib/security/bash-tool-context.js +9 -3
  95. package/lib/security/bash-tool-context.js.map +1 -1
  96. package/lib/security/exec-policy.d.ts.map +1 -1
  97. package/lib/security/exec-policy.js +4 -1
  98. package/lib/security/exec-policy.js.map +1 -1
  99. package/lib/subagent.d.ts +7 -1
  100. package/lib/subagent.d.ts.map +1 -1
  101. package/lib/subagent.js +22 -8
  102. package/lib/subagent.js.map +1 -1
  103. package/lib/utils/config-env.d.ts +5 -0
  104. package/lib/utils/config-env.d.ts.map +1 -0
  105. package/lib/utils/config-env.js +34 -0
  106. package/lib/utils/config-env.js.map +1 -0
  107. package/lib/zhin-agent/config.d.ts +23 -0
  108. package/lib/zhin-agent/config.d.ts.map +1 -1
  109. package/lib/zhin-agent/config.js +20 -0
  110. package/lib/zhin-agent/config.js.map +1 -1
  111. package/lib/zhin-agent/index.d.ts +21 -0
  112. package/lib/zhin-agent/index.d.ts.map +1 -1
  113. package/lib/zhin-agent/index.js +257 -35
  114. package/lib/zhin-agent/index.js.map +1 -1
  115. package/lib/zhin-agent/prompt.d.ts +6 -0
  116. package/lib/zhin-agent/prompt.d.ts.map +1 -1
  117. package/lib/zhin-agent/prompt.js +70 -15
  118. package/lib/zhin-agent/prompt.js.map +1 -1
  119. package/lib/zhin-agent/tool-catalog.d.ts +15 -0
  120. package/lib/zhin-agent/tool-catalog.d.ts.map +1 -0
  121. package/lib/zhin-agent/tool-catalog.js +52 -0
  122. package/lib/zhin-agent/tool-catalog.js.map +1 -0
  123. package/lib/zhin-agent/tool-runtime.d.ts +3 -1
  124. package/lib/zhin-agent/tool-runtime.d.ts.map +1 -1
  125. package/lib/zhin-agent/tool-runtime.js +14 -0
  126. package/lib/zhin-agent/tool-runtime.js.map +1 -1
  127. package/lib/zhin-agent/tool-search-orchestrator.d.ts +23 -0
  128. package/lib/zhin-agent/tool-search-orchestrator.d.ts.map +1 -0
  129. package/lib/zhin-agent/tool-search-orchestrator.js +86 -0
  130. package/lib/zhin-agent/tool-search-orchestrator.js.map +1 -0
  131. package/lib/zhin-agent/turn-metrics.d.ts +20 -0
  132. package/lib/zhin-agent/turn-metrics.d.ts.map +1 -0
  133. package/lib/zhin-agent/turn-metrics.js +28 -0
  134. package/lib/zhin-agent/turn-metrics.js.map +1 -0
  135. package/package.json +14 -4
  136. package/lib/builtin/read-memory-tool.d.ts +0 -14
  137. package/lib/builtin/read-memory-tool.d.ts.map +0 -1
  138. package/lib/builtin/read-memory-tool.js +0 -49
  139. package/lib/builtin/read-memory-tool.js.map +0 -1
  140. package/lib/builtin/write-memory-tool.d.ts +0 -14
  141. package/lib/builtin/write-memory-tool.d.ts.map +0 -1
  142. package/lib/builtin/write-memory-tool.js +0 -50
  143. package/lib/builtin/write-memory-tool.js.map +0 -1
@@ -15,28 +15,36 @@
15
15
  * 11. 定时任务:cron_add 持久化定时任务
16
16
  * 12. 多模态输入:图片/音频直接传给视觉模型
17
17
  */
18
- import { Logger, getPlugin } from '@zhin.js/core';
18
+ import { getPlugin, Logger } from '@zhin.js/core';
19
+ import { formatCompact, truncatePreview } from '@zhin.js/logger';
20
+ import { ensureMcpConnections } from '../orchestrator/mcp-lifecycle.js';
19
21
  import { createAgent } from '@zhin.js/ai';
20
22
  import { SessionManager, createMemorySessionManager } from '@zhin.js/ai';
21
23
  import { ConversationMemory } from '@zhin.js/ai';
22
24
  import { parseOutput } from '@zhin.js/ai';
25
+ import { addUsage, EMPTY_USAGE, } from './turn-metrics.js';
23
26
  import { UserProfileStore } from '../user-profile.js';
24
27
  import { WEB_SEARCH_LOCALE_EXTRA_KEY, normalizeWebSearchLocaleHint, } from '../builtin/web-search-locale.js';
25
28
  import { RateLimiter } from '@zhin.js/ai';
26
29
  import { detectTone } from '@zhin.js/ai';
27
30
  import { SubagentManager } from '../subagent.js';
28
31
  import { triggerAIHook, createAIHookEvent } from '../hooks.js';
32
+ import { resolveAgentPromptMarkdown } from '../agent-prompt/index.js';
29
33
  // ── Sub-modules ─────────────────────────────────────────────────────
30
34
  import { DEFAULT_CONFIG, isPhaseTraceEnabled, } from './config.js';
31
35
  import { applyExecPolicyToTools } from '../security/exec-policy.js';
32
36
  import { runWithBashToolContext } from '../security/bash-tool-context.js';
33
- import { buildEnhancedPersona, buildContextHint, buildRichSystemPrompt, buildUserMessageWithHistory, FIXED_DISCIPLINE_RULES, } from './prompt.js';
37
+ import { buildEnhancedPersona, buildContextHint, buildRichSystemPrompt, buildLiteSystemPromptWithPlatform, buildUserMessageWithHistory, FIXED_DISCIPLINE_RULES, } from './prompt.js';
34
38
  import { buildPreExecFastPathPrompt, collectRuntimeTools, planToolRun, } from './tool-runtime.js';
39
+ import { DeferredWorkerRunner } from '../deferred-worker-runner.js';
40
+ import { buildOrchestratorAgentTools } from './tool-search-orchestrator.js';
41
+ import { filterToolsForToolSearchCatalog } from './tool-catalog.js';
35
42
  import { stripHallucinatedToolCalls, stripThinkBlocks } from './text-sanitize.js';
36
43
  import { pruneHistoryWithBudget } from './context-budget.js';
37
44
  import { resolveModelHarness } from './model-harness.js';
38
45
  import { RESERVED_TOOL_NAMES, RESERVED_TOOL_NAME_PREFIXES } from '../reserved-tools.js';
39
46
  import { createOwnerOrchestratedToolResultTransform } from '../orchestrator/owner-confirm-orchestration.js';
47
+ export { formatAiHandlerCompleteLog, formatZhinAgentTurnUsage } from './turn-metrics.js';
40
48
  const logger = new Logger(null, 'ZhinAgent');
41
49
  const now = () => performance.now();
42
50
  // ============================================================================
@@ -46,6 +54,7 @@ export class ZhinAgent {
46
54
  provider;
47
55
  config;
48
56
  skillRegistry = null;
57
+ orchestrator = null;
49
58
  sessions;
50
59
  contextManager = null;
51
60
  memory;
@@ -60,6 +69,12 @@ export class ZhinAgent {
60
69
  phaseTraceEnabled;
61
70
  /** 根插件(createZhinAgentContext 注入);避免在 Agent.run 路径依赖 AsyncLocalStorage 上的 getPlugin() */
62
71
  hostPlugin = null;
72
+ deferredCatalog = [];
73
+ deferredWorkerRunner = new DeferredWorkerRunner();
74
+ lastToolSearchDeferredStats;
75
+ lastTurnMetrics = null;
76
+ activeTurnSubagentUsage = null;
77
+ activeTurnSubagentWaits = [];
63
78
  constructor(provider, config) {
64
79
  this.provider = provider;
65
80
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -79,6 +94,10 @@ export class ZhinAgent {
79
94
  this.skillRegistry = registry;
80
95
  logger.debug(`SkillRegistry connected (${registry.size} skills)`);
81
96
  }
97
+ setOrchestrator(orchestrator) {
98
+ this.orchestrator = orchestrator;
99
+ logger.debug('AgentOrchestrator connected for MCP and resources');
100
+ }
82
101
  setSessionManager(manager) {
83
102
  this.sessions.dispose();
84
103
  this.sessions = manager;
@@ -110,6 +129,8 @@ export class ZhinAgent {
110
129
  maxIterations: this.config.maxSubagentIterations,
111
130
  execPolicyConfig: this.config,
112
131
  modelRegistry: this.modelRegistry,
132
+ onSubagentUsage: (usage) => this.addActiveTurnSubagentUsage(usage),
133
+ registerSubagentTask: (done) => this.trackActiveTurnSubagent(done),
113
134
  });
114
135
  logger.debug('SubagentManager initialized');
115
136
  }
@@ -170,6 +191,51 @@ export class ZhinAgent {
170
191
  setSkillsSummaryXML(xml) {
171
192
  this.skillsSummaryXML = xml || '';
172
193
  }
194
+ /** 上一轮 `process` / `processMultimodal` 的 token 等指标(供 AI Handler 打汇总日志) */
195
+ getLastTurnMetrics() {
196
+ return this.lastTurnMetrics;
197
+ }
198
+ beginActiveTurn() {
199
+ this.activeTurnSubagentUsage = { ...EMPTY_USAGE };
200
+ this.activeTurnSubagentWaits = [];
201
+ }
202
+ addActiveTurnSubagentUsage(usage) {
203
+ if (!this.activeTurnSubagentUsage)
204
+ return;
205
+ addUsage(this.activeTurnSubagentUsage, usage);
206
+ }
207
+ trackActiveTurnSubagent(done) {
208
+ if (!this.activeTurnSubagentUsage)
209
+ return;
210
+ this.activeTurnSubagentWaits.push(done);
211
+ }
212
+ async finalizeActiveTurn(partial) {
213
+ const waitMs = this.config.subagentTurnWaitMs;
214
+ if (this.activeTurnSubagentWaits.length > 0 && waitMs > 0) {
215
+ await Promise.race([
216
+ Promise.allSettled(this.activeTurnSubagentWaits),
217
+ new Promise(resolve => setTimeout(resolve, waitMs)),
218
+ ]);
219
+ }
220
+ const mainUsage = { ...partial.usage };
221
+ const subagentUsage = this.activeTurnSubagentUsage
222
+ && (this.activeTurnSubagentUsage.total_tokens > 0
223
+ || this.activeTurnSubagentUsage.prompt_tokens > 0
224
+ || this.activeTurnSubagentUsage.completion_tokens > 0)
225
+ ? { ...this.activeTurnSubagentUsage }
226
+ : undefined;
227
+ const totalUsage = { ...mainUsage };
228
+ if (subagentUsage)
229
+ addUsage(totalUsage, subagentUsage);
230
+ this.lastTurnMetrics = {
231
+ ...partial,
232
+ usage: totalUsage,
233
+ mainUsage,
234
+ ...(subagentUsage ? { subagentUsage } : {}),
235
+ };
236
+ this.activeTurnSubagentUsage = null;
237
+ this.activeTurnSubagentWaits = [];
238
+ }
173
239
  // ── Core processing ─────────────────────────────────────────────────
174
240
  async process(content, context, externalTools = [], onChunk) {
175
241
  const t0 = now();
@@ -185,8 +251,10 @@ export class ZhinAgent {
185
251
  if (!rateCheck.allowed) {
186
252
  this.logPhase('turn.rate_limited', sessionId, { userId });
187
253
  logger.debug(`[速率限制] 用户 ${userId} 被限制: ${rateCheck.message}`);
254
+ await this.finalizeActiveTurn({ usage: EMPTY_USAGE, path: 'rate_limited' });
188
255
  return parseOutput(rateCheck.message || '请稍后再试');
189
256
  }
257
+ this.beginActiveTurn();
190
258
  // 0.5 工具上下文:web_search 语言(档案 preferred_language / language,否则默认中文)
191
259
  const contextForTools = await this.attachWebSearchLocale(context, userId);
192
260
  triggerAIHook(createAIHookEvent('message', 'received', sessionId, {
@@ -194,8 +262,13 @@ export class ZhinAgent {
194
262
  content,
195
263
  platform: platform || '',
196
264
  })).catch(() => { });
265
+ // 0.9 Lazy-connect configured MCP servers before tool collection
266
+ if (this.orchestrator) {
267
+ await ensureMcpConnections(this.orchestrator.mcps);
268
+ }
197
269
  // 1. Collect tools
198
270
  const tFilter = now();
271
+ const mcpTools = this.orchestrator?.mcps.getAllMcpTools() ?? [];
199
272
  const allTools = collectRuntimeTools({
200
273
  content,
201
274
  context: contextForTools,
@@ -208,10 +281,17 @@ export class ZhinAgent {
208
281
  memory: this.memory,
209
282
  userProfiles: this.userProfiles,
210
283
  subagentManager: this.subagentManager,
284
+ mcpTools,
211
285
  });
286
+ const { tools: resolvedTools, deferredStats } = this.resolveAgentToolsForTurn(allTools, contextForTools);
287
+ this.lastToolSearchDeferredStats = deferredStats;
212
288
  const filterMs = (now() - tFilter).toFixed(0);
213
- this.logPhase('tools.collected', sessionId, { count: allTools.length });
214
- logger.info(`[工具过滤] ${allTools.length} 个工具: ${allTools.map(t => t.name).join(', ') || '(无)'}`);
289
+ this.logPhase('tools.collected', sessionId, { count: resolvedTools.length });
290
+ logger.debug(formatCompact({
291
+ tools: resolvedTools.length,
292
+ tool_search: this.config.toolSearch || undefined,
293
+ names: resolvedTools.map(t => t.name).join(',') || '(none)',
294
+ }));
215
295
  // 2. History + profile (parallel)
216
296
  const tMem = now();
217
297
  const [rawHistoryMessages, profileSummary] = await Promise.all([
@@ -239,16 +319,33 @@ export class ZhinAgent {
239
319
  this.logPhase('path.chat', sessionId, { toolCount: 0 });
240
320
  const liteModel = this.config.chatLiteModel || undefined;
241
321
  const chatSystemPrompt = this.buildDisciplinedPrompt(personaEnhanced);
242
- logger.info(`[System Prompt] chat-path: ${chatSystemPrompt.length} chars${liteModel ? `, model=${liteModel}` : ''}`);
243
- logger.debug(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms (${historyMessages.length}条), 0 工具`);
322
+ logger.debug(formatCompact({
323
+ mode: 'chat',
324
+ prompt_chars: chatSystemPrompt.length,
325
+ model: liteModel || chatCandidates[0] || undefined,
326
+ }));
244
327
  const tLLM = now();
245
328
  this.logPhase('chat.llm.start', sessionId, { model: liteModel || chatCandidates[0] || '' });
246
- let reply = await this.streamChatWithHistory(content, chatSystemPrompt, historyMessages, onChunk, liteModel);
247
- reply = stripHallucinatedToolCalls(reply);
329
+ const chatResult = await this.streamChatWithHistory(content, chatSystemPrompt, historyMessages, onChunk, liteModel);
330
+ let reply = stripHallucinatedToolCalls(chatResult.content);
248
331
  const llmMs = (now() - tLLM).toFixed(0);
249
- this.logPhase('chat.llm.end', sessionId, { durationMs: Number(llmMs) });
250
- logger.info(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${llmMs}ms, 总=${(now() - t0).toFixed(0)}ms`);
332
+ this.logPhase('chat.llm.end', sessionId, {
333
+ durationMs: Number(llmMs),
334
+ ...this.usageLogFields(chatResult.usage ?? undefined),
335
+ });
336
+ logger.debug(formatCompact({
337
+ mode: 'chat',
338
+ filter_ms: filterMs,
339
+ mem_ms: memMs,
340
+ llm_ms: llmMs,
341
+ total_ms: Math.round(now() - t0),
342
+ }));
251
343
  await this.saveToSession(sessionId, content, reply, sceneId);
344
+ await this.finalizeActiveTurn({
345
+ usage: chatResult.usage ?? EMPTY_USAGE,
346
+ path: 'chat',
347
+ model: chatResult.model,
348
+ });
252
349
  this.logPhase('turn.end', sessionId, { path: 'chat' });
253
350
  return parseOutput(reply);
254
351
  }
@@ -259,7 +356,7 @@ export class ZhinAgent {
259
356
  if (preExecCandidates.length > 0) {
260
357
  logger.debug(`预执行: ${preExecCandidates.map(t => t.name).join(', ')}`);
261
358
  }
262
- const toolRun = await planToolRun(allTools, this.config.preExecTimeout);
359
+ const toolRun = await planToolRun(resolvedTools, this.config.preExecTimeout);
263
360
  this.logPhase('preexec.done', sessionId, {
264
361
  mode: toolRun.mode,
265
362
  preExecutedTools: toolRun.preExecution.tools.length,
@@ -275,30 +372,52 @@ export class ZhinAgent {
275
372
  // Fast path
276
373
  const tLLM = now();
277
374
  const prompt = this.buildDisciplinedPrompt(buildPreExecFastPathPrompt(personaEnhanced, preData));
278
- logger.info(`[System Prompt] fast-path: ${prompt.length} chars`);
375
+ logger.debug(formatCompact({ mode: 'fast', prompt_chars: prompt.length }));
279
376
  this.logPhase('fast.llm.start', sessionId, { model: chatCandidates[0] || '' });
280
- reply = await this.streamChatWithHistory(content, prompt, historyMessages, onChunk);
281
- this.logPhase('fast.llm.end', sessionId, { durationMs: Math.round(now() - tLLM) });
282
- logger.info(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
377
+ const fastResult = await this.streamChatWithHistory(content, prompt, historyMessages, onChunk);
378
+ reply = fastResult.content;
379
+ this.logPhase('fast.llm.end', sessionId, {
380
+ durationMs: Math.round(now() - tLLM),
381
+ ...this.usageLogFields(fastResult.usage ?? undefined),
382
+ });
383
+ logger.debug(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
384
+ await this.finalizeActiveTurn({
385
+ usage: fastResult.usage ?? EMPTY_USAGE,
386
+ path: 'fast',
387
+ model: fastResult.model,
388
+ });
283
389
  }
284
390
  else {
285
391
  this.logPhase('path.agent', sessionId, { toolCount: allTools.length });
286
392
  const tAgent = now();
287
393
  logger.debug(`Agent 路径: ${allTools.length} 个工具`);
288
394
  const contextHint = buildContextHint(context, content);
395
+ const platformMarkdown = await resolveAgentPromptMarkdown({
396
+ ctx: {
397
+ slot: 'orchestrator',
398
+ toolContext: context,
399
+ toolSearch: !!this.config.toolSearch,
400
+ userMessagePreview: content.slice(0, 500),
401
+ deferred: deferredStats ? { goal: content, domainStats: deferredStats } : undefined,
402
+ },
403
+ config: this.config,
404
+ sessionId,
405
+ });
289
406
  const richPrompt = buildRichSystemPrompt({
290
407
  config: this.config,
291
408
  skillRegistry: this.skillRegistry,
292
409
  skillsSummaryXML: this.skillsSummaryXML,
293
410
  activeSkillsContext: this.activeSkillsContext,
294
411
  bootstrapContext: this.bootstrapContext,
412
+ toolSearchDeferredStats: deferredStats,
413
+ platformSections: platformMarkdown,
295
414
  });
296
415
  const systemPrompt = `${richPrompt}
297
416
  ${contextHint}
298
417
  ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
299
- logger.info(`[System Prompt] ${systemPrompt.length} chars`);
418
+ logger.debug(formatCompact({ mode: 'agent', prompt_chars: systemPrompt.length }));
300
419
  logger.debug(`[System Prompt Full]\n${systemPrompt}\n---END---`);
301
- const agentTools = applyExecPolicyToTools(this.config, allTools);
420
+ const agentTools = applyExecPolicyToTools(this.config, resolvedTools);
302
421
  // Adaptive maxIterations: boost when skills are active (multi-step skill flows)
303
422
  const SKILL_ITERATION_BOOST = 3;
304
423
  const hasSkillActivation = agentTools.some(t => t.name === 'activate_skill' || t.name === 'install_skill');
@@ -318,7 +437,7 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
318
437
  orchestrationPlugin = getPlugin().root ?? getPlugin();
319
438
  }
320
439
  catch {
321
- logger.warn('[ZhinAgent] 无 hostPlugin 且 getPlugin() 不可用,Owner 硬编排可能无法自动 ask_user');
440
+ logger.warn(formatCompact({ warn: 'no_host_plugin' }));
322
441
  }
323
442
  }
324
443
  const agent = createAgent(this.provider, {
@@ -342,13 +461,23 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
342
461
  try {
343
462
  this.logPhase('agent.run.start', sessionId, { model: chatCandidates[0] || '' });
344
463
  result = await runWithBashToolContext(contextForTools, () => agent.run(userMessageWithHistory, []));
345
- this.logPhase('agent.run.end', sessionId, { iterations: result.iterations });
464
+ this.logPhase('agent.run.end', sessionId, {
465
+ iterations: result.iterations,
466
+ durationMs: Math.round(now() - tAgent),
467
+ ...this.usageLogFields(result.usage),
468
+ });
346
469
  }
347
470
  finally {
348
471
  agent.dispose();
349
472
  }
350
473
  reply = stripHallucinatedToolCalls(stripThinkBlocks(result.content)) || this.fallbackFormat(result.toolCalls);
351
- logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
474
+ logger.debug(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
475
+ await this.finalizeActiveTurn({
476
+ usage: result.usage,
477
+ path: 'agent',
478
+ iterations: result.iterations,
479
+ model: chatCandidates[0] || undefined,
480
+ });
352
481
  }
353
482
  await this.saveToSession(sessionId, content, reply, sceneId);
354
483
  triggerAIHook(createAIHookEvent('message', 'sent', sessionId, {
@@ -359,14 +488,71 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
359
488
  this.logPhase('turn.end', sessionId, { path: toolRun.mode === 'pre-exec-fast-path' ? 'fast' : 'agent' });
360
489
  return parseOutput(reply);
361
490
  }
491
+ resolveAgentToolsForTurn(allTools, context) {
492
+ if (!this.config.toolSearch) {
493
+ return { tools: allTools };
494
+ }
495
+ const toolSearchPool = filterToolsForToolSearchCatalog(allTools);
496
+ const built = buildOrchestratorAgentTools({
497
+ allTools: toolSearchPool,
498
+ config: this.config,
499
+ context,
500
+ getDeferredCatalog: () => this.deferredCatalog,
501
+ runWorker: (goal, toolQuery) => this.runDeferredWorker(goal, toolQuery, context, toolSearchPool),
502
+ });
503
+ this.deferredCatalog = built.deferred;
504
+ logger.debug(formatCompact({
505
+ tool_search: `${built.orchestratorTools.length}+${built.deferred.length}`,
506
+ stats: built.domainStats,
507
+ }));
508
+ return { tools: built.orchestratorTools, deferredStats: built.domainStats };
509
+ }
510
+ async runDeferredWorker(goal, toolQuery, context, allTools) {
511
+ const allByName = new Map(allTools.map(t => [t.name, t]));
512
+ const workerBase = [];
513
+ for (const name of this.config.toolSearchWorkerBaseTools) {
514
+ const t = allByName.get(name);
515
+ if (t)
516
+ workerBase.push(t);
517
+ }
518
+ const result = await this.deferredWorkerRunner.runSync({
519
+ goal,
520
+ toolQuery,
521
+ deferredCatalog: this.deferredCatalog,
522
+ workerBaseTools: workerBase,
523
+ allToolsByName: allByName,
524
+ origin: context,
525
+ maxToolResults: this.config.toolSearchMaxResults,
526
+ execPolicyConfig: this.config,
527
+ modelRegistry: this.modelRegistry,
528
+ provider: this.provider,
529
+ maxIterations: this.config.maxSubagentIterations,
530
+ });
531
+ return result.summary;
532
+ }
362
533
  buildDisciplinedPrompt(basePrompt) {
363
534
  const discipline = `# Discipline\n${FIXED_DISCIPLINE_RULES.map(rule => `- ${rule}`).join('\n')}`;
364
535
  return `${discipline}\n\n${basePrompt}`;
365
536
  }
537
+ usageLogFields(usage) {
538
+ if (!usage)
539
+ return {};
540
+ return {
541
+ promptTokens: usage.prompt_tokens,
542
+ completionTokens: usage.completion_tokens,
543
+ totalTokens: usage.total_tokens,
544
+ };
545
+ }
366
546
  logPhase(phase, sessionId, extra = {}) {
367
547
  if (!this.phaseTraceEnabled)
368
548
  return;
369
- logger.info({ phase, sessionId, ...extra }, '[AGENT_PHASE]');
549
+ const flat = { phase, session: sessionId };
550
+ for (const [k, v] of Object.entries(extra)) {
551
+ if (v === undefined || v === null)
552
+ continue;
553
+ flat[k] = typeof v === 'object' ? JSON.stringify(v) : v;
554
+ }
555
+ logger.info(formatCompact(flat));
370
556
  }
371
557
  async processMultimodal(parts, context, onChunk) {
372
558
  const { senderId, sceneId, platform } = context;
@@ -374,8 +560,10 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
374
560
  const userId = senderId || 'unknown';
375
561
  const rateCheck = this.rateLimiter.check(userId);
376
562
  if (!rateCheck.allowed) {
563
+ await this.finalizeActiveTurn({ usage: EMPTY_USAGE, path: 'rate_limited' });
377
564
  return parseOutput(rateCheck.message || '请稍后再试');
378
565
  }
566
+ this.beginActiveTurn();
379
567
  const rawHistoryMessages = await this.buildHistoryMessages(sessionId);
380
568
  const profileSummary = await this.userProfiles.buildProfileSummary(userId);
381
569
  const personaEnhanced = this.buildDisciplinedPrompt(buildEnhancedPersona(this.config, profileSummary, ''));
@@ -408,6 +596,17 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
408
596
  }
409
597
  }
410
598
  const textContent = textFragments.join(' ') || '[多模态消息]';
599
+ const platformMarkdown = await resolveAgentPromptMarkdown({
600
+ ctx: {
601
+ slot: 'orchestrator',
602
+ toolContext: context,
603
+ toolSearch: !!this.config.toolSearch,
604
+ userMessagePreview: textContent.slice(0, 500),
605
+ },
606
+ config: this.config,
607
+ sessionId,
608
+ });
609
+ const visionSystemPrompt = buildLiteSystemPromptWithPlatform(personaEnhanced, platformMarkdown, buildContextHint(context, textContent));
411
610
  const visionCandidates = this.resolveModelCandidates('vision');
412
611
  const { messages: historyMessages, result: pruneResult } = pruneHistoryWithBudget({
413
612
  messages: rawHistoryMessages,
@@ -420,16 +619,21 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
420
619
  logger.debug(`[多模态上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
421
620
  }
422
621
  const messages = [
423
- { role: 'system', content: personaEnhanced },
622
+ { role: 'system', content: visionSystemPrompt },
424
623
  ...historyMessages,
425
624
  { role: 'user', content: llmParts },
426
625
  ];
427
626
  let reply = '';
627
+ let lastUsage = null;
628
+ let usedVisionModel = visionCandidates[0] || '';
428
629
  for (let i = 0; i < visionCandidates.length; i++) {
429
630
  const visionModel = visionCandidates[i];
631
+ usedVisionModel = visionModel;
430
632
  try {
431
633
  reply = '';
432
634
  for await (const chunk of this.provider.chatStream({ model: visionModel, messages })) {
635
+ if (chunk.usage)
636
+ lastUsage = chunk.usage;
433
637
  const delta = chunk.choices?.[0]?.delta;
434
638
  if (!delta)
435
639
  continue;
@@ -442,8 +646,10 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
442
646
  }
443
647
  reply = stripThinkBlocks(reply);
444
648
  if (!reply) {
445
- logger.warn(`[processMultimodal] ${visionModel} 流式为空,尝试非流式`);
649
+ logger.warn(formatCompact({ mode: 'multimodal', fallback: visionModel, reason: 'empty_stream' }));
446
650
  const response = await this.provider.chat({ model: visionModel, messages });
651
+ if (response.usage)
652
+ lastUsage = response.usage;
447
653
  const msg = response.choices[0]?.message?.content;
448
654
  reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
449
655
  }
@@ -455,13 +661,19 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
455
661
  if (isLast) {
456
662
  try {
457
663
  const response = await this.provider.chat({ model: visionModel, messages });
664
+ if (response.usage)
665
+ lastUsage = response.usage;
458
666
  const msg = response.choices[0]?.message?.content;
459
667
  reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
460
668
  }
461
669
  catch { /* all candidates exhausted */ }
462
670
  }
463
671
  else {
464
- logger.warn(`[processMultimodal] ${visionModel} 失败,降级到 ${visionCandidates[i + 1]}: ${err.message}`);
672
+ logger.warn(formatCompact({
673
+ mode: 'multimodal',
674
+ fallback: `${visionModel}→${visionCandidates[i + 1]}`,
675
+ error: truncatePreview(err.message, 80),
676
+ }));
465
677
  }
466
678
  }
467
679
  }
@@ -469,6 +681,11 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
469
681
  reply = '抱歉,我无法理解这条消息。';
470
682
  reply = stripHallucinatedToolCalls(reply);
471
683
  await this.saveToSession(sessionId, textContent, reply, sceneId);
684
+ await this.finalizeActiveTurn({
685
+ usage: lastUsage ?? EMPTY_USAGE,
686
+ path: 'multimodal',
687
+ model: usedVisionModel,
688
+ });
472
689
  return parseOutput(reply);
473
690
  }
474
691
  // ── Internal helpers ────────────────────────────────────────────────
@@ -500,6 +717,7 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
500
717
  }
501
718
  async streamChatWithHistory(content, systemPrompt, history, onChunk, preferredModel) {
502
719
  const candidates = this.resolveModelCandidates('chat', preferredModel);
720
+ const empty = (model) => ({ content: '', usage: null, model });
503
721
  const turnTimeout = this.config.timeout;
504
722
  const userContent = history.length > 0
505
723
  ? buildUserMessageWithHistory(history, content)
@@ -535,21 +753,19 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
535
753
  }
536
754
  }
537
755
  })());
538
- if (lastUsage) {
539
- logger.info(`[闲聊] token 用量: prompt=${lastUsage.prompt_tokens}, completion=${lastUsage.completion_tokens}, total=${lastUsage.total_tokens} (model=${model})`);
540
- }
541
756
  result = stripThinkBlocks(result);
542
- if (result)
543
- return result;
757
+ if (result) {
758
+ return { content: result, usage: lastUsage, model };
759
+ }
544
760
  // Streaming returned empty content — try non-streaming with same model
545
- logger.warn(`[streamChat] ${model} 流式响应为空,尝试非流式`);
761
+ logger.warn(formatCompact({ stream: 'empty', model }));
546
762
  const response = await withTurnTimeout(this.provider.chat({ model, messages }));
547
763
  const msg = response.choices?.[0]?.message?.content;
548
764
  result = stripThinkBlocks(typeof msg === 'string' ? msg : '');
549
765
  if (result) {
550
766
  if (onChunk)
551
767
  onChunk(result, result);
552
- return result;
768
+ return { content: result, usage: response.usage ?? null, model };
553
769
  }
554
770
  }
555
771
  catch (err) {
@@ -562,16 +778,22 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
562
778
  let result = stripThinkBlocks(typeof msg === 'string' ? msg : '');
563
779
  if (onChunk && result)
564
780
  onChunk(result, result);
565
- return result;
781
+ if (result) {
782
+ return { content: result, usage: response.usage ?? null, model };
783
+ }
784
+ return empty(model);
566
785
  }
567
786
  catch {
568
- return '';
787
+ return empty(model);
569
788
  }
570
789
  }
571
- logger.warn(`[streamChat] ${model} 失败,降级到 ${candidates[i + 1]}: ${err.message}`);
790
+ logger.warn(formatCompact({
791
+ fallback: `${model}→${candidates[i + 1]}`,
792
+ error: truncatePreview(err.message, 80),
793
+ }));
572
794
  }
573
795
  }
574
- return '';
796
+ return empty(candidates[0] || '');
575
797
  }
576
798
  async saveToSession(sessionId, userContent, assistantContent, sceneId) {
577
799
  await this.memory.saveRound(sessionId, userContent, assistantContent);