@zhin.js/agent 0.1.30 → 0.1.31

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 (132) hide show
  1. package/README.md +47 -22
  2. package/lib/builtin/run-deferred-task-tool.d.ts +1 -1
  3. package/lib/builtin/run-deferred-task-tool.d.ts.map +1 -1
  4. package/lib/builtin/run-deferred-task-tool.js +1 -1
  5. package/lib/builtin/run-deferred-task-tool.js.map +1 -1
  6. package/lib/config/normalize-ai-config.d.ts +4 -0
  7. package/lib/config/normalize-ai-config.d.ts.map +1 -1
  8. package/lib/config/normalize-ai-config.js +23 -6
  9. package/lib/config/normalize-ai-config.js.map +1 -1
  10. package/lib/config/provider-instance.d.ts.map +1 -1
  11. package/lib/config/provider-instance.js +26 -7
  12. package/lib/config/provider-instance.js.map +1 -1
  13. package/lib/config/types.d.ts +2 -1
  14. package/lib/config/types.d.ts.map +1 -1
  15. package/lib/config/types.js.map +1 -1
  16. package/lib/config/validate-ai-config.d.ts.map +1 -1
  17. package/lib/config/validate-ai-config.js +5 -0
  18. package/lib/config/validate-ai-config.js.map +1 -1
  19. package/lib/deferred-worker-runner.d.ts +2 -0
  20. package/lib/deferred-worker-runner.d.ts.map +1 -1
  21. package/lib/deferred-worker-runner.js +17 -34
  22. package/lib/deferred-worker-runner.js.map +1 -1
  23. package/lib/index.d.ts +2 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +1 -1
  26. package/lib/index.js.map +1 -1
  27. package/lib/init/activate-ai-database-storage.d.ts +1 -1
  28. package/lib/init/activate-ai-database-storage.d.ts.map +1 -1
  29. package/lib/init/activate-ai-database-storage.js +16 -13
  30. package/lib/init/activate-ai-database-storage.js.map +1 -1
  31. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  32. package/lib/init/create-zhin-agent.js +17 -2
  33. package/lib/init/create-zhin-agent.js.map +1 -1
  34. package/lib/init/register-db-models.d.ts.map +1 -1
  35. package/lib/init/register-db-models.js +7 -8
  36. package/lib/init/register-db-models.js.map +1 -1
  37. package/lib/init/register-db-upgrade.d.ts +1 -1
  38. package/lib/init/register-db-upgrade.js +2 -2
  39. package/lib/init/register-db-upgrade.js.map +1 -1
  40. package/lib/init/register-management-tools.js +1 -1
  41. package/lib/init/register-management-tools.js.map +1 -1
  42. package/lib/media/subagent-user-delivery.d.ts +2 -9
  43. package/lib/media/subagent-user-delivery.d.ts.map +1 -1
  44. package/lib/media/subagent-user-delivery.js.map +1 -1
  45. package/lib/service.d.ts +31 -22
  46. package/lib/service.d.ts.map +1 -1
  47. package/lib/service.js +48 -19
  48. package/lib/service.js.map +1 -1
  49. package/lib/subagent-ai-events.d.ts +2 -2
  50. package/lib/subagent-ai-events.d.ts.map +1 -1
  51. package/lib/subagent-ai-events.js +17 -21
  52. package/lib/subagent-ai-events.js.map +1 -1
  53. package/lib/subagent.d.ts.map +1 -1
  54. package/lib/subagent.js +44 -59
  55. package/lib/subagent.js.map +1 -1
  56. package/lib/zhin-agent/agent-loop-standalone.d.ts +32 -0
  57. package/lib/zhin-agent/agent-loop-standalone.d.ts.map +1 -0
  58. package/lib/zhin-agent/agent-loop-standalone.js +144 -0
  59. package/lib/zhin-agent/agent-loop-standalone.js.map +1 -0
  60. package/lib/zhin-agent/agent-loop-turn.d.ts +51 -0
  61. package/lib/zhin-agent/agent-loop-turn.d.ts.map +1 -0
  62. package/lib/zhin-agent/agent-loop-turn.js +287 -0
  63. package/lib/zhin-agent/agent-loop-turn.js.map +1 -0
  64. package/lib/zhin-agent/builtin-tools.d.ts +3 -2
  65. package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
  66. package/lib/zhin-agent/builtin-tools.js +15 -16
  67. package/lib/zhin-agent/builtin-tools.js.map +1 -1
  68. package/lib/zhin-agent/config.d.ts +5 -2
  69. package/lib/zhin-agent/config.d.ts.map +1 -1
  70. package/lib/zhin-agent/config.js +3 -2
  71. package/lib/zhin-agent/config.js.map +1 -1
  72. package/lib/zhin-agent/deferred-delivery.d.ts +5 -0
  73. package/lib/zhin-agent/deferred-delivery.d.ts.map +1 -0
  74. package/lib/zhin-agent/deferred-delivery.js +29 -0
  75. package/lib/zhin-agent/deferred-delivery.js.map +1 -0
  76. package/lib/zhin-agent/index.d.ts +40 -8
  77. package/lib/zhin-agent/index.d.ts.map +1 -1
  78. package/lib/zhin-agent/index.js +162 -17
  79. package/lib/zhin-agent/index.js.map +1 -1
  80. package/lib/zhin-agent/model-resolver.d.ts +0 -1
  81. package/lib/zhin-agent/model-resolver.d.ts.map +1 -1
  82. package/lib/zhin-agent/model-resolver.js.map +1 -1
  83. package/lib/zhin-agent/multimodal-message.d.ts +9 -0
  84. package/lib/zhin-agent/multimodal-message.d.ts.map +1 -0
  85. package/lib/zhin-agent/multimodal-message.js +69 -0
  86. package/lib/zhin-agent/multimodal-message.js.map +1 -0
  87. package/lib/zhin-agent/prompt-access.d.ts +4 -0
  88. package/lib/zhin-agent/prompt-access.d.ts.map +1 -0
  89. package/lib/zhin-agent/prompt-access.js +7 -0
  90. package/lib/zhin-agent/prompt-access.js.map +1 -0
  91. package/lib/zhin-agent/prompt-controller.d.ts +53 -0
  92. package/lib/zhin-agent/prompt-controller.d.ts.map +1 -0
  93. package/lib/zhin-agent/prompt-controller.js +165 -0
  94. package/lib/zhin-agent/prompt-controller.js.map +1 -0
  95. package/lib/zhin-agent/prompt-input.d.ts +3 -0
  96. package/lib/zhin-agent/prompt-input.d.ts.map +1 -0
  97. package/lib/zhin-agent/prompt-input.js +8 -0
  98. package/lib/zhin-agent/prompt-input.js.map +1 -0
  99. package/lib/zhin-agent/session-io.d.ts +12 -7
  100. package/lib/zhin-agent/session-io.d.ts.map +1 -1
  101. package/lib/zhin-agent/session-io.js +18 -15
  102. package/lib/zhin-agent/session-io.js.map +1 -1
  103. package/lib/zhin-agent/session-message-queue.d.ts +20 -0
  104. package/lib/zhin-agent/session-message-queue.d.ts.map +1 -0
  105. package/lib/zhin-agent/session-message-queue.js +52 -0
  106. package/lib/zhin-agent/session-message-queue.js.map +1 -0
  107. package/lib/zhin-agent/tool-calls-user-format.d.ts.map +1 -1
  108. package/lib/zhin-agent/tool-calls-user-format.js +3 -0
  109. package/lib/zhin-agent/tool-calls-user-format.js.map +1 -1
  110. package/lib/zhin-agent/tool-orchestration.d.ts.map +1 -1
  111. package/lib/zhin-agent/tool-orchestration.js +35 -5
  112. package/lib/zhin-agent/tool-orchestration.js.map +1 -1
  113. package/lib/zhin-agent/tool-runtime.d.ts +2 -2
  114. package/lib/zhin-agent/tool-runtime.d.ts.map +1 -1
  115. package/lib/zhin-agent/tool-runtime.js +4 -4
  116. package/lib/zhin-agent/tool-runtime.js.map +1 -1
  117. package/lib/zhin-agent/turn-context.d.ts +9 -0
  118. package/lib/zhin-agent/turn-context.d.ts.map +1 -0
  119. package/lib/zhin-agent/turn-context.js +12 -0
  120. package/lib/zhin-agent/turn-context.js.map +1 -0
  121. package/lib/zhin-agent/turn-pipeline.d.ts +5 -1
  122. package/lib/zhin-agent/turn-pipeline.d.ts.map +1 -1
  123. package/lib/zhin-agent/turn-pipeline.js +113 -436
  124. package/lib/zhin-agent/turn-pipeline.js.map +1 -1
  125. package/lib/zhin-agent/zhin-agent-private.d.ts +11 -4
  126. package/lib/zhin-agent/zhin-agent-private.d.ts.map +1 -1
  127. package/lib/zhin-agent/zhin-agent-private.js.map +1 -1
  128. package/package.json +3 -3
  129. package/lib/zhin-agent/llm-runner.d.ts +0 -18
  130. package/lib/zhin-agent/llm-runner.d.ts.map +0 -1
  131. package/lib/zhin-agent/llm-runner.js +0 -163
  132. package/lib/zhin-agent/llm-runner.js.map +0 -1
@@ -1,37 +1,32 @@
1
- import { getPlugin } from '@zhin.js/core';
2
- import { formatCompact, truncatePreview, Logger } from '@zhin.js/logger';
3
- import { createAgent } from '@zhin.js/ai';
1
+ import { formatCompact, Logger } from '@zhin.js/logger';
2
+ import { createUserMessage } from '@zhin.js/ai';
4
3
  import { resolveIMSessionIdFromToolContext } from '@zhin.js/ai';
5
4
  import { parseOutput } from '@zhin.js/ai';
6
5
  import { detectTone } from '@zhin.js/ai';
7
6
  import { ensureMcpConnectionsForBinding, getMcpToolsForBinding, } from '../orchestrator/mcp-lifecycle.js';
8
- import { applyExecPolicyToTools } from '../security/exec-policy.js';
9
- import { runWithBashToolContext } from '../security/bash-tool-context.js';
10
7
  import { triggerAIHook, createAIHookEvent } from '../hooks.js';
11
8
  import { resolveModelCandidates } from './model-resolver.js';
12
- import { streamChatWithHistory, streamChatWithVisionMedia } from './llm-runner.js';
13
- import { INBOUND_MEDIA_PARTS_EXTRA_KEY } from '../media/constants.js';
14
9
  import { mergeToolOutboundElements } from '../media/media-tool-bridge.js';
15
10
  import { providerSupportsVision } from '../media/vision-capability.js';
16
- import { buildSessionCreateInput, touchSession, buildHistoryMessages, formatUserContentForSession, resolveSessionIsNewBeforeCreate, } from './session-io.js';
11
+ import { touchSession, formatUserContentForSession, resolveSessionIsNewBeforeCreate, beginTurnSession as beginTurnSessionIO, } from './session-io.js';
12
+ import { runAgentLoopTextTurn, runAgentLoopVisionTurn } from './agent-loop-turn.js';
17
13
  import { buildEnhancedPersona } from './prompt.js';
18
- import { collectRuntimeTools, planToolRun } from './tool-runtime.js';
19
- import { sanitizeAssistantReply, stripThinkBlocks } from './text-sanitize.js';
20
- import { pruneHistoryWithBudget } from './context-budget.js';
14
+ import { collectRuntimeTools } from './tool-runtime.js';
21
15
  import { buildMultimodalVisionSystemPrompt } from './prompt-assembly.js';
22
- import { formatToolCallsForUser } from './tool-calls-user-format.js';
23
- import { resolveModelHarness } from './model-harness.js';
24
- import { RESERVED_TOOL_NAMES, RESERVED_TOOL_NAME_PREFIXES } from '../reserved-tools.js';
25
- import { createOwnerOrchestratedToolResultTransform } from '../orchestrator/owner-confirm-orchestration.js';
26
- import { logPhase, usageLogFields } from './phase-trace.js';
27
16
  import { attachWebSearchLocale } from './web-search-locale-attach.js';
28
17
  import { EMPTY_USAGE } from './turn-metrics.js';
29
18
  import { resolveAgentToolsForTurn } from './tool-orchestration.js';
30
- import { buildAgentPathSystemPrompt, buildChatPathSystemPrompt, buildFastPathSystemPrompt, buildAgentUserMessage, } from './prompt-assembly.js';
19
+ import { logPhase } from './phase-trace.js';
20
+ import { buildVisionUserMessage, summarizeMultimodalParts } from './multimodal-message.js';
21
+ import { DEFAULT_MULTIMODAL_CONFIG } from '../media/media-types.js';
31
22
  const logger = new Logger(null, 'ZhinAgent');
32
23
  const now = () => performance.now();
33
24
  function sessionDeps(host) {
34
- return { chatHistory: host.chatHistory, imSessionStore: host.imSessionStore };
25
+ return {
26
+ imSessionStore: host.imSessionStore,
27
+ agentSessionStore: host.agentSessionStore,
28
+ contextRepository: host.contextRepository,
29
+ };
35
30
  }
36
31
  async function beginTurnSession(host, context) {
37
32
  const sessionKey = resolveIMSessionIdFromToolContext({
@@ -41,22 +36,21 @@ async function beginTurnSession(host, context) {
41
36
  sceneId: context.sceneId,
42
37
  senderId: context.senderId,
43
38
  });
44
- const imSession = await host.imSessionStore.getOrCreateActive(buildSessionCreateInput(sessionKey, context));
45
- return { sessionKey, sessionId: imSession.session_id };
39
+ return beginTurnSessionIO(sessionDeps(host), sessionKey, context);
46
40
  }
47
- export async function processTextTurn(host, content, context, externalTools = [], onChunk) {
41
+ export async function processTextTurn(host, content, context, externalTools = [], onChunk, extras) {
48
42
  const t0 = now();
49
- const { senderId, sceneId, platform, botId, messageId } = context;
43
+ const { senderId, platform, botId } = context;
50
44
  const sessionUserContent = formatUserContentForSession(context, content);
51
45
  const userId = senderId || 'unknown';
52
46
  const sessionKey = resolveIMSessionIdFromToolContext({
53
47
  platform,
54
48
  botId,
55
49
  scope: context.scope,
56
- sceneId,
50
+ sceneId: context.sceneId,
57
51
  senderId,
58
52
  });
59
- const isNewSession = await resolveSessionIsNewBeforeCreate(sessionDeps(host), sessionKey, context);
53
+ const isNewSession = await resolveSessionIsNewBeforeCreate(sessionDeps(host), sessionKey);
60
54
  await host.waitForMemoryPersistence();
61
55
  const { sessionId } = await beginTurnSession(host, context);
62
56
  await host.emitter.dispatch('ai.processing.start', host.emitter.createPayload(sessionId, context, 'text', {
@@ -66,7 +60,6 @@ export async function processTextTurn(host, content, context, externalTools = []
66
60
  mode: 'text',
67
61
  provider: host.getTurnProvider().name,
68
62
  });
69
- // 0. Rate limit
70
63
  const rateCheck = host.rateLimiter.check(userId);
71
64
  if (!rateCheck.allowed) {
72
65
  logPhase(host.phaseConfig, 'turn.rate_limited', sessionId, { userId });
@@ -79,12 +72,10 @@ export async function processTextTurn(host, content, context, externalTools = []
79
72
  await host.finalizeActiveTurn({ usage: EMPTY_USAGE, path: 'rate_limited' });
80
73
  return parseOutput(rateCheck.message || '请稍后再试');
81
74
  }
82
- // 0.1 发射 typing 事件,由适配器插件自行决定如何响应
83
75
  host.emitter.emit('ai.typing.start', host.emitter.createPayload(sessionId, context, 'text', {
84
76
  reason: 'processing',
85
77
  }));
86
78
  host.beginActiveTurn();
87
- // 0.5 工具上下文:web_search 语言(档案 preferred_language / language,否则默认中文)
88
79
  const contextForTools = await attachWebSearchLocale(context, userId, host.userProfiles);
89
80
  triggerAIHook(createAIHookEvent('message', 'received', sessionId, {
90
81
  userId,
@@ -93,7 +84,6 @@ export async function processTextTurn(host, content, context, externalTools = []
93
84
  })).catch(() => { });
94
85
  const turnBinding = host.activeBinding;
95
86
  const mcpServerNames = turnBinding?.mcpServers ?? [];
96
- // 0.9 Lazy-connect MCP servers listed on active agent binding
97
87
  if (host.orchestrator && mcpServerNames.length > 0) {
98
88
  await ensureMcpConnectionsForBinding(host.orchestrator.mcps, mcpServerNames, (event) => {
99
89
  const payload = host.emitter.createPayload(sessionId, contextForTools, 'text', {
@@ -114,12 +104,11 @@ export async function processTextTurn(host, content, context, externalTools = []
114
104
  }
115
105
  });
116
106
  }
117
- // 1. Collect tools
118
107
  const tFilter = now();
119
108
  const mcpTools = host.orchestrator && mcpServerNames.length > 0
120
109
  ? getMcpToolsForBinding(host.orchestrator.mcps, mcpServerNames)
121
110
  : [];
122
- let allTools = collectRuntimeTools({
111
+ const allTools = collectRuntimeTools({
123
112
  content,
124
113
  context: contextForTools,
125
114
  externalTools,
@@ -128,7 +117,7 @@ export async function processTextTurn(host, content, context, externalTools = []
128
117
  externalRegistered: host.externalTools,
129
118
  sessionId,
130
119
  userId,
131
- chatHistory: host.chatHistory,
120
+ imTranscriptStore: host.imTranscriptStore,
132
121
  userProfiles: host.userProfiles,
133
122
  mcpTools,
134
123
  });
@@ -141,323 +130,91 @@ export async function processTextTurn(host, content, context, externalTools = []
141
130
  tool_search: true,
142
131
  names: resolvedTools.map(t => t.name).join(',') || '(none)',
143
132
  }));
144
- // 2. History + profile (parallel)
145
- const tMem = now();
146
- const [rawHistoryMessages, profileSummary] = await Promise.all([
147
- buildHistoryMessages(sessionDeps(host), sessionId, context, sessionUserContent),
148
- host.userProfiles.buildProfileSummary(userId),
149
- ]);
150
- const chatCandidates = resolveModelCandidates(host.getTurnProvider().models, host.modelRegistry, host.getTurnProvider().name, host.config, 'chat');
151
- const { messages: historyMessages, result: pruneResult, budget: contextBudget, } = pruneHistoryWithBudget({
152
- messages: rawHistoryMessages,
153
- config: host.config,
154
- provider: host.getTurnProvider(),
155
- modelRegistry: host.modelRegistry,
156
- model: chatCandidates[0],
157
- });
158
- if (pruneResult.droppedCount > 0) {
159
- logger.debug(`[上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
160
- }
161
- const memMs = (now() - tMem).toFixed(0);
162
- logPhase(host.phaseConfig, 'context.ready', sessionId, { historyCount: historyMessages.length });
163
- // 2.5 Tone + persona
133
+ const profileSummary = await host.userProfiles.buildProfileSummary(userId);
164
134
  const toneHint = host.config.toneAwareness ? detectTone(content).hint : '';
165
135
  const personaEnhanced = buildEnhancedPersona(host.config, profileSummary, toneHint);
166
- // 3. No tools chat path (prefer per-session model, then lightweight model)
167
- if (allTools.length === 0) {
168
- logPhase(host.phaseConfig, 'path.chat', sessionId, { toolCount: 0 });
169
- const liteModel = host.config.chatLiteModel || undefined;
170
- const chatSystemPrompt = buildChatPathSystemPrompt(host, personaEnhanced, contextForTools);
171
- logger.debug(formatCompact({
172
- mode: 'chat',
173
- prompt_chars: chatSystemPrompt.length,
174
- model: liteModel || chatCandidates[0] || undefined,
175
- }));
176
- const tLLM = now();
177
- logPhase(host.phaseConfig, 'chat.llm.start', sessionId, { model: liteModel || chatCandidates[0] || '' });
178
- const inboundVision = contextForTools.extra?.[INBOUND_MEDIA_PARTS_EXTRA_KEY];
179
- const chatResult = inboundVision?.length
180
- ? await streamChatWithVisionMedia({ provider: host.getTurnProvider(), modelRegistry: host.modelRegistry, config: host.config }, sessionUserContent, inboundVision, chatSystemPrompt, historyMessages, onChunk, liteModel || host.config.visionModel)
181
- : await streamChatWithHistory({ provider: host.getTurnProvider(), modelRegistry: host.modelRegistry, config: host.config }, sessionUserContent, chatSystemPrompt, historyMessages, onChunk, liteModel);
182
- let reply = sanitizeAssistantReply(chatResult.content);
183
- await host.emitter.dispatch('ai.response', host.emitter.createPayload(sessionId, context, 'text', {
184
- path: 'chat',
185
- model: chatResult.model,
186
- reply,
187
- }));
188
- const llmMs = (now() - tLLM).toFixed(0);
189
- logPhase(host.phaseConfig, 'chat.llm.end', sessionId, {
190
- durationMs: Number(llmMs),
191
- ...usageLogFields(chatResult.usage ?? undefined),
192
- });
193
- logger.debug(formatCompact({
194
- mode: 'chat',
195
- filter_ms: filterMs,
196
- mem_ms: memMs,
197
- llm_ms: llmMs,
198
- total_ms: Math.round(now() - t0),
199
- }));
200
- await touchSession(sessionDeps(host), sessionId);
201
- if (isNewSession) {
202
- host.emitSessionNewEvent(sessionId, context, 'text', sessionUserContent, reply);
203
- }
204
- await host.finalizeActiveTurn({
205
- usage: chatResult.usage ?? EMPTY_USAGE,
206
- path: 'chat',
207
- model: chatResult.model,
208
- });
209
- await host.emitter.dispatch('ai.processing.finish', host.emitter.createPayload(sessionId, context, 'text', {
210
- path: 'chat',
211
- model: chatResult.model,
212
- reply,
213
- }));
214
- host.emitter.emit('ai.typing.stop', host.emitter.createPayload(sessionId, context, 'text', {
215
- reason: 'processing_complete',
216
- }));
217
- logPhase(host.phaseConfig, 'turn.end', sessionId, { path: 'chat' });
218
- return parseOutput(reply);
219
- }
220
- logger.debug(`[工具路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, ${allTools.length} 工具 (${allTools.map(t => t.name).join(', ')})`);
221
- // 4. Pre-executable tools
222
- const preExecCandidates = allTools.filter(tool => tool.preExecutable);
223
- const tPre = preExecCandidates.length > 0 ? now() : 0;
224
- if (preExecCandidates.length > 0) {
225
- logger.debug(`预执行: ${preExecCandidates.map(t => t.name).join(', ')}`);
226
- }
227
- const toolRun = await planToolRun(resolvedTools, host.config.preExecTimeout);
228
- logPhase(host.phaseConfig, 'preexec.done', sessionId, {
229
- mode: toolRun.mode,
230
- preExecutedTools: toolRun.preExecution.tools.length,
231
- });
232
- const preData = toolRun.preExecution.data;
233
- if (tPre > 0) {
234
- logger.debug(`预执行耗时: ${(now() - tPre).toFixed(0)}ms`);
235
- }
236
- // 6. Path selection
237
- let reply;
238
- let agentToolCalls = [];
239
- if (toolRun.mode === 'pre-exec-fast-path') {
240
- logPhase(host.phaseConfig, 'path.pre_exec_fast', sessionId, { toolCount: allTools.length });
241
- // Fast path
242
- const tLLM = now();
243
- const prompt = buildFastPathSystemPrompt(host, personaEnhanced, preData, contextForTools);
244
- logger.debug(formatCompact({ mode: 'fast', prompt_chars: prompt.length }));
245
- logPhase(host.phaseConfig, 'fast.llm.start', sessionId, { model: chatCandidates[0] || '' });
246
- const fastResult = await streamChatWithHistory({ provider: host.getTurnProvider(), modelRegistry: host.modelRegistry, config: host.config }, sessionUserContent, prompt, historyMessages, onChunk);
247
- reply = sanitizeAssistantReply(fastResult.content);
248
- logPhase(host.phaseConfig, 'fast.llm.end', sessionId, {
249
- durationMs: Math.round(now() - tLLM),
250
- ...usageLogFields(fastResult.usage ?? undefined),
251
- });
252
- logger.debug(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
253
- await host.finalizeActiveTurn({
254
- usage: fastResult.usage ?? EMPTY_USAGE,
255
- path: 'fast',
256
- model: fastResult.model,
257
- });
258
- }
259
- else {
260
- logPhase(host.phaseConfig, 'path.agent', sessionId, { toolCount: allTools.length });
261
- const tAgent = now();
262
- logger.debug(`Agent 路径: ${allTools.length} 个工具`);
263
- const systemPrompt = await buildAgentPathSystemPrompt(host, {
264
- content,
265
- context: contextForTools,
136
+ const chatCandidates = resolveModelCandidates(host.getTurnProvider().models, host.modelRegistry, host.getTurnProvider().name, host.config, 'chat');
137
+ logPhase(host.phaseConfig, 'path.agent_loop', sessionId, { toolCount: allTools.length });
138
+ const userMessages = extras?.prebuiltMessages?.length
139
+ ? extras.prebuiltMessages
140
+ : [createUserMessage(sessionUserContent)];
141
+ const loopResult = await host.promptController.schedule({
142
+ sessionKey,
143
+ sessionId,
144
+ userMessages,
145
+ context,
146
+ onChunk,
147
+ execute: (initialMessages, hooks, signal, _turnId) => runAgentLoopTextTurn({
148
+ host,
266
149
  sessionId,
150
+ sessionUserContent,
151
+ rawContent: content,
152
+ context,
153
+ contextForTools,
154
+ allTools,
155
+ resolvedTools,
267
156
  personaEnhanced,
268
- preData,
157
+ modelId: chatCandidates[0] || host.getTurnProvider().models[0] || 'gpt-4o-mini',
158
+ modelCandidates: chatCandidates,
159
+ onChunk,
160
+ initialMessages,
161
+ promptHooks: hooks,
162
+ signal,
269
163
  deferredStats,
270
- });
271
- logger.debug(formatCompact({ mode: 'agent', prompt_chars: systemPrompt.length }));
272
- logger.debug(`[System Prompt Full]\n${systemPrompt}\n---END---`);
273
- const agentTools = applyExecPolicyToTools(host.config, resolvedTools, {
274
- approvalMode: host.config.execApprovalMode,
275
- });
276
- // Adaptive maxIterations: boost when skills are active (multi-step skill flows)
277
- const SKILL_ITERATION_BOOST = 3;
278
- const hasSkillActivation = agentTools.some(t => t.name === 'activate_skill' || t.name === 'install_skill');
279
- const harness = resolveModelHarness(host.getTurnProvider().name, chatCandidates[0] || '', host.config.modelHarness);
280
- const baseIterations = harness.maxIterations ?? host.config.maxIterations;
281
- const effectiveMaxIterations = hasSkillActivation
282
- ? baseIterations + SKILL_ITERATION_BOOST
283
- : baseIterations;
284
- logPhase(host.phaseConfig, 'harness.resolved', sessionId, {
285
- model: chatCandidates[0] || '',
286
- harnessMaxIterations: harness.maxIterations ?? null,
287
- effectiveMaxIterations,
288
- });
289
- let orchestrationPlugin = host.emitter.getHostPlugin() ?? undefined;
290
- if (!orchestrationPlugin) {
291
- try {
292
- orchestrationPlugin = getPlugin().root ?? getPlugin();
293
- }
294
- catch {
295
- logger.warn(formatCompact({ warn: 'no_host_plugin' }));
296
- }
297
- }
298
- const llmAgent = createAgent(host.getTurnProvider(), {
299
- model: chatCandidates[0],
300
- modelFallbacks: chatCandidates.slice(1),
301
- systemPrompt,
302
- tools: agentTools,
303
- maxIterations: effectiveMaxIterations,
304
- turnTimeout: host.config.timeout,
305
- contextWindow: contextBudget.contextWindow,
306
- reservedToolNames: RESERVED_TOOL_NAMES,
307
- reservedToolNamePrefixes: RESERVED_TOOL_NAME_PREFIXES,
308
- transformToolResult: createOwnerOrchestratedToolResultTransform({
309
- toolContext: contextForTools,
310
- disableHardOrchestration: false,
311
- plugin: orchestrationPlugin,
312
- }),
313
- policyDenialStopAfter: host.config.policyDenialStopAfter,
314
- });
315
- llmAgent.on('thinking', (message) => {
316
- host.emitter.emit('ai.thinking', host.emitter.createPayload(sessionId, contextForTools, 'text', {
317
- path: 'agent',
318
- thinking: message,
319
- }));
320
- });
321
- llmAgent.on('tool_call', (toolName, args) => {
322
- host.emitter.emit('ai.tool.call', host.emitter.createPayload(sessionId, contextForTools, 'text', {
323
- path: 'agent',
324
- toolName,
325
- args,
326
- }));
327
- });
328
- llmAgent.on('tool_result', (toolName, result) => {
329
- host.emitter.emit('ai.tool.result', host.emitter.createPayload(sessionId, contextForTools, 'text', {
330
- path: 'agent',
331
- toolName,
332
- result,
333
- }));
334
- });
335
- llmAgent.on('compaction', (info) => {
336
- host.emitSessionCompactEvent(sessionId, contextForTools, 'text', info);
337
- });
338
- const userMessageWithHistory = buildAgentUserMessage(historyMessages, sessionUserContent);
339
- let result;
340
- try {
341
- await host.emitter.dispatch('ai.agent.start', host.emitter.createPayload(sessionId, contextForTools, 'text', {
342
- path: 'agent',
343
- model: chatCandidates[0] || undefined,
344
- }));
345
- logPhase(host.phaseConfig, 'agent.run.start', sessionId, { model: chatCandidates[0] || '' });
346
- result = await runWithBashToolContext(contextForTools, () => llmAgent.run(userMessageWithHistory, []));
347
- logPhase(host.phaseConfig, 'agent.run.end', sessionId, {
348
- iterations: result.iterations,
349
- durationMs: Math.round(now() - tAgent),
350
- ...usageLogFields(result.usage),
351
- });
352
- await host.emitter.dispatch('ai.agent.finish', host.emitter.createPayload(sessionId, contextForTools, 'text', {
353
- path: 'agent',
354
- model: result.model ?? (chatCandidates[0] || undefined),
355
- iterations: result.iterations,
356
- }));
357
- }
358
- catch (error) {
359
- await host.emitter.dispatch('ai.processing.error', host.emitter.createPayload(sessionId, contextForTools, 'text', {
360
- path: 'agent',
361
- error: error instanceof Error ? error.message : String(error),
362
- }));
363
- throw error;
364
- }
365
- finally {
366
- llmAgent.dispose();
367
- }
368
- agentToolCalls = result.toolCalls;
369
- const spawnedSubagent = result.toolCalls.some(tc => tc.tool === 'spawn_task');
370
- if (spawnedSubagent) {
371
- await host.turnTracker.waitForPendingSubagents();
372
- }
373
- const delegatedOnly = spawnedSubagent
374
- && !result.toolCalls.some(tc => tc.tool === 'run_deferred_task')
375
- && !result.toolCalls.some(tc => tc.tool === 'generate_image'
376
- && tc.result
377
- && typeof tc.result === 'object'
378
- && typeof tc.result.image === 'string');
379
- if (delegatedOnly) {
380
- reply = '';
381
- }
382
- else {
383
- reply = sanitizeAssistantReply(result.content, {
384
- toolSummary: formatToolCallsForUser(result.toolCalls),
385
- });
386
- }
387
- await host.emitter.dispatch('ai.response', host.emitter.createPayload(sessionId, contextForTools, 'text', {
388
- path: 'agent',
389
- model: result.model ?? (chatCandidates[0] || undefined),
390
- iterations: result.iterations,
391
- reply,
392
- }));
393
- logger.debug(formatCompact({
394
- agent_answer: truncatePreview(reply, 480),
395
- tool_calls: result.toolCalls.length,
396
- ...(result.toolCalls.length
397
- ? { tools: result.toolCalls.map(tc => tc.tool).join(',') }
398
- : {}),
399
- }));
400
- for (const tc of result.toolCalls) {
401
- const preview = toolCallResultDebugPreview(tc);
402
- logger.debug(formatCompact({
403
- tool_result: tc.tool,
404
- preview: truncatePreview(preview, 480),
405
- }));
406
- }
407
- logger.debug(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
408
- await host.finalizeActiveTurn({
409
- usage: result.usage,
410
- path: 'agent',
411
- iterations: result.iterations,
412
- model: result.model ?? (chatCandidates[0] || undefined),
413
- });
414
- }
164
+ }),
165
+ });
166
+ const reply = loopResult.reply;
167
+ await host.emitter.dispatch('ai.response', host.emitter.createPayload(sessionId, context, 'text', {
168
+ path: loopResult.path,
169
+ model: loopResult.model,
170
+ iterations: loopResult.iterations,
171
+ reply,
172
+ }));
415
173
  await touchSession(sessionDeps(host), sessionId);
416
174
  if (isNewSession) {
417
175
  host.emitSessionNewEvent(sessionId, context, 'text', sessionUserContent, reply);
418
176
  }
177
+ await host.finalizeActiveTurn({
178
+ usage: loopResult.usage,
179
+ path: loopResult.path,
180
+ iterations: loopResult.iterations,
181
+ model: loopResult.model,
182
+ });
419
183
  triggerAIHook(createAIHookEvent('message', 'sent', sessionId, {
420
184
  userId,
421
185
  content: reply,
422
186
  platform: platform || '',
423
187
  })).catch(() => { });
424
188
  await host.emitter.dispatch('ai.processing.finish', host.emitter.createPayload(sessionId, context, 'text', {
425
- path: toolRun.mode === 'pre-exec-fast-path' ? 'fast' : 'agent',
189
+ path: loopResult.path,
190
+ model: loopResult.model,
426
191
  reply,
427
192
  }));
428
- logPhase(host.phaseConfig, 'turn.end', sessionId, { path: toolRun.mode === 'pre-exec-fast-path' ? 'fast' : 'agent' });
429
193
  host.emitter.emit('ai.typing.stop', host.emitter.createPayload(sessionId, context, 'text', {
430
194
  reason: 'processing_complete',
431
195
  }));
432
- const outbound = mergeToolOutboundElements(parseOutput(reply), agentToolCalls);
196
+ logPhase(host.phaseConfig, 'turn.end', sessionId, {
197
+ path: loopResult.path,
198
+ filter_ms: filterMs,
199
+ total_ms: Math.round(now() - t0),
200
+ });
201
+ const outbound = mergeToolOutboundElements(parseOutput(reply), loopResult.toolCalls);
433
202
  if (!reply.trim() && outbound.length === 0)
434
203
  return [];
435
204
  return outbound;
436
205
  }
437
- function toolCallResultDebugPreview(tc) {
438
- if (tc.tool === 'generate_image' && tc.result && typeof tc.result === 'object' && !Array.isArray(tc.result)) {
439
- const image = tc.result.image;
440
- const len = typeof image === 'string' ? image.length : 0;
441
- return len > 0 ? `[image generated, ${len} b64 chars]` : '[generate_image: no image field]';
442
- }
443
- if (tc.tool === 'voice_tts' && tc.result && typeof tc.result === 'object' && !Array.isArray(tc.result)) {
444
- const audio = tc.result.audio;
445
- const len = typeof audio === 'string' ? audio.length : 0;
446
- return len > 0 ? `[audio generated, ${len} b64 chars]` : '[voice_tts: no audio field]';
447
- }
448
- return typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result);
449
- }
206
+ /** @deprecated 仅保留类型引用;多模态 turn 已统一 agentLoop */
450
207
  export async function processMultimodalTurn(host, parts, context, onChunk) {
451
- const { senderId, sceneId, platform, botId } = context;
208
+ const { senderId, platform, botId } = context;
452
209
  const userId = senderId || 'unknown';
453
210
  const sessionKey = resolveIMSessionIdFromToolContext({
454
211
  platform,
455
212
  botId,
456
213
  scope: context.scope,
457
- sceneId,
214
+ sceneId: context.sceneId,
458
215
  senderId,
459
216
  });
460
- const isNewSession = await resolveSessionIsNewBeforeCreate(sessionDeps(host), sessionKey, context);
217
+ const isNewSession = await resolveSessionIsNewBeforeCreate(sessionDeps(host), sessionKey);
461
218
  await host.waitForMemoryPersistence();
462
219
  const { sessionId } = await beginTurnSession(host, context);
463
220
  await host.emitter.dispatch('ai.processing.start', host.emitter.createPayload(sessionId, context, 'multimodal'));
@@ -475,54 +232,11 @@ export async function processMultimodalTurn(host, parts, context, onChunk) {
475
232
  reason: 'processing',
476
233
  }));
477
234
  host.beginActiveTurn();
478
- const textFragmentsEarly = [];
479
- for (const p of parts) {
480
- if (p.type === 'text')
481
- textFragmentsEarly.push(p.text);
482
- }
483
- const textContentEarly = textFragmentsEarly.join(' ') || '[多模态消息]';
484
- const sessionUserContentEarly = formatUserContentForSession(context, textContentEarly);
485
- const rawHistoryMessages = await buildHistoryMessages(sessionDeps(host), sessionId, context, sessionUserContentEarly);
486
- const profileSummary = await host.userProfiles.buildProfileSummary(userId);
487
- const personaEnhanced = host.buildDisciplinedPrompt(buildEnhancedPersona(host.config, profileSummary, ''));
488
- const textFragments = [];
489
- const llmParts = [];
490
235
  const supportsVision = providerSupportsVision(host.getTurnProvider());
491
- for (const p of parts) {
492
- switch (p.type) {
493
- case 'text':
494
- textFragments.push(p.text);
495
- llmParts.push(p);
496
- break;
497
- case 'image_url':
498
- textFragments.push('[图片]');
499
- if (supportsVision)
500
- llmParts.push(p);
501
- else
502
- llmParts.push({ type: 'text', text: '[用户发送了一张图片]' });
503
- break;
504
- case 'video_url':
505
- textFragments.push('[视频]');
506
- llmParts.push({ type: 'text', text: `[用户发送了一个视频: ${p.video_url.url}]` });
507
- break;
508
- case 'audio':
509
- textFragments.push('[音频]');
510
- if (supportsVision)
511
- llmParts.push(p);
512
- else
513
- llmParts.push({ type: 'text', text: '[用户发送了一段音频]' });
514
- break;
515
- case 'face':
516
- textFragments.push(p.face.text || `[表情:${p.face.id}]`);
517
- llmParts.push({
518
- type: 'text',
519
- text: p.face.text ? `[表情: ${p.face.text}]` : `[表情ID: ${p.face.id}]`,
520
- });
521
- break;
522
- }
523
- }
524
- const textContent = textFragments.join(' ') || '[多模态消息]';
236
+ const textContent = summarizeMultimodalParts(parts, supportsVision);
525
237
  const sessionUserContent = formatUserContentForSession(context, textContent);
238
+ const profileSummary = await host.userProfiles.buildProfileSummary(userId);
239
+ const personaEnhanced = host.buildDisciplinedPrompt(buildEnhancedPersona(host.config, profileSummary, ''));
526
240
  const visionSystemPrompt = await buildMultimodalVisionSystemPrompt(host, {
527
241
  context,
528
242
  sessionId,
@@ -530,81 +244,43 @@ export async function processMultimodalTurn(host, parts, context, onChunk) {
530
244
  personaEnhanced,
531
245
  });
532
246
  const visionCandidates = resolveModelCandidates(host.getTurnProvider().models, host.modelRegistry, host.getTurnProvider().name, host.config, 'vision');
533
- const { messages: historyMessages, result: pruneResult } = pruneHistoryWithBudget({
534
- messages: rawHistoryMessages,
535
- config: host.config,
536
- provider: host.getTurnProvider(),
537
- modelRegistry: host.modelRegistry,
538
- model: visionCandidates[0],
539
- });
540
- if (pruneResult.droppedCount > 0) {
541
- logger.debug(`[多模态上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
247
+ logPhase(host.phaseConfig, 'path.agent_loop', sessionId, { mode: 'multimodal' });
248
+ const userMessage = await buildVisionUserMessage(sessionUserContent, parts, supportsVision, DEFAULT_MULTIMODAL_CONFIG.maxFileBytes);
249
+ let loopResult;
250
+ try {
251
+ loopResult = await host.promptController.schedule({
252
+ sessionKey,
253
+ sessionId,
254
+ userMessages: [userMessage],
255
+ context,
256
+ onChunk,
257
+ execute: (initialMessages, hooks, signal, _turnId) => runAgentLoopVisionTurn({
258
+ host,
259
+ sessionId,
260
+ context,
261
+ visionSystemPrompt,
262
+ userMessages: initialMessages,
263
+ modelCandidates: visionCandidates,
264
+ onChunk,
265
+ promptHooks: hooks,
266
+ signal,
267
+ }),
268
+ });
542
269
  }
543
- const messages = [
544
- { role: 'system', content: visionSystemPrompt },
545
- ...historyMessages,
546
- { role: 'user', content: llmParts },
547
- ];
548
- let reply = '';
549
- let lastUsage = null;
550
- let usedVisionModel = visionCandidates[0] || '';
551
- for (let i = 0; i < visionCandidates.length; i++) {
552
- const visionModel = visionCandidates[i];
553
- usedVisionModel = visionModel;
554
- try {
555
- reply = '';
556
- for await (const chunk of host.getTurnProvider().chatStream({ model: visionModel, messages })) {
557
- if (chunk.usage)
558
- lastUsage = chunk.usage;
559
- const delta = chunk.choices?.[0]?.delta;
560
- if (!delta)
561
- continue;
562
- const text = typeof delta.content === 'string' ? delta.content : '';
563
- if (text) {
564
- reply += text;
565
- if (onChunk)
566
- onChunk(text, reply);
567
- }
568
- }
569
- reply = stripThinkBlocks(reply);
570
- if (!reply) {
571
- logger.warn(formatCompact({ mode: 'multimodal', fallback: visionModel, reason: 'empty_stream' }));
572
- const response = await host.getTurnProvider().chat({ model: visionModel, messages });
573
- if (response.usage)
574
- lastUsage = response.usage;
575
- const msg = response.choices[0]?.message?.content;
576
- reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
577
- }
578
- if (reply)
579
- break;
580
- }
581
- catch (err) {
582
- const isLast = i === visionCandidates.length - 1;
583
- if (isLast) {
584
- try {
585
- const response = await host.getTurnProvider().chat({ model: visionModel, messages });
586
- if (response.usage)
587
- lastUsage = response.usage;
588
- const msg = response.choices[0]?.message?.content;
589
- reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
590
- }
591
- catch { /* all candidates exhausted */ }
592
- }
593
- else {
594
- logger.warn(formatCompact({
595
- mode: 'multimodal',
596
- fallback: `${visionModel}→${visionCandidates[i + 1]}`,
597
- error: truncatePreview(err.message),
598
- }));
599
- }
600
- }
270
+ catch {
271
+ loopResult = {
272
+ reply: '抱歉,我无法理解这条消息。',
273
+ usage: EMPTY_USAGE,
274
+ path: 'multimodal',
275
+ iterations: 0,
276
+ model: visionCandidates[0] || '',
277
+ toolCalls: [],
278
+ };
601
279
  }
602
- if (!reply)
603
- reply = '抱歉,我无法理解这条消息。';
604
- reply = sanitizeAssistantReply(reply);
280
+ const reply = loopResult.reply;
605
281
  await host.emitter.dispatch('ai.response', host.emitter.createPayload(sessionId, context, 'multimodal', {
606
282
  path: 'multimodal',
607
- model: usedVisionModel,
283
+ model: loopResult.model,
608
284
  reply,
609
285
  }));
610
286
  await touchSession(sessionDeps(host), sessionId);
@@ -612,13 +288,14 @@ export async function processMultimodalTurn(host, parts, context, onChunk) {
612
288
  host.emitSessionNewEvent(sessionId, context, 'multimodal', sessionUserContent, reply);
613
289
  }
614
290
  await host.finalizeActiveTurn({
615
- usage: lastUsage ?? EMPTY_USAGE,
291
+ usage: loopResult.usage,
616
292
  path: 'multimodal',
617
- model: usedVisionModel,
293
+ model: loopResult.model,
294
+ iterations: loopResult.iterations,
618
295
  });
619
296
  await host.emitter.dispatch('ai.processing.finish', host.emitter.createPayload(sessionId, context, 'multimodal', {
620
297
  path: 'multimodal',
621
- model: usedVisionModel,
298
+ model: loopResult.model,
622
299
  reply,
623
300
  }));
624
301
  host.emitter.emit('ai.typing.stop', host.emitter.createPayload(sessionId, context, 'multimodal', {