autosnippet 3.2.8 → 3.2.10

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 (114) hide show
  1. package/bin/cli.js +6 -5
  2. package/dashboard/dist/assets/index-BTAsOZv2.js +128 -0
  3. package/dashboard/dist/assets/index-C_72Ct98.css +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +23 -26
  6. package/lib/cli/SetupService.js +1 -1
  7. package/lib/cli/deploy/FileManifest.js +1 -1
  8. package/lib/core/AstAnalyzer.js +1 -1
  9. package/lib/core/discovery/index.js +2 -2
  10. package/lib/external/ai/AiProvider.js +66 -172
  11. package/lib/external/ai/providers/GoogleGeminiProvider.js +29 -5
  12. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +1 -1
  13. package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +3 -3
  14. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +1 -1
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +1 -1
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +8 -8
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +291 -204
  19. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +7 -6
  20. package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +1 -1
  21. package/lib/external/mcp/handlers/bootstrap-internal.js +2 -2
  22. package/lib/external/mcp/handlers/dimension-complete-external.js +6 -6
  23. package/lib/http/HttpServer.js +1 -1
  24. package/lib/http/middleware/requestLogger.js +1 -0
  25. package/lib/http/routes/ai.js +240 -35
  26. package/lib/http/routes/candidates.js +2 -3
  27. package/lib/http/routes/extract.js +13 -11
  28. package/lib/http/routes/modules.js +2 -2
  29. package/lib/http/routes/recipes.js +5 -5
  30. package/lib/http/routes/remote.js +134 -255
  31. package/lib/http/routes/violations.js +0 -54
  32. package/lib/http/utils/sse-sessions.js +1 -1
  33. package/lib/infrastructure/logging/Logger.js +5 -4
  34. package/lib/infrastructure/monitoring/PerformanceMonitor.js +3 -2
  35. package/lib/injection/ServiceContainer.js +64 -17
  36. package/lib/platform/ScreenCaptureService.js +177 -0
  37. package/lib/platform/ios/routes/spm.js +2 -2
  38. package/lib/service/agent/AgentEventBus.js +207 -0
  39. package/lib/service/agent/AgentFactory.js +535 -0
  40. package/lib/service/agent/AgentMessage.js +240 -0
  41. package/lib/service/agent/AgentRouter.js +228 -0
  42. package/lib/service/agent/AgentRuntime.js +1056 -0
  43. package/lib/service/agent/AgentState.js +217 -0
  44. package/lib/service/agent/IntentClassifier.js +331 -0
  45. package/lib/service/agent/LarkTransport.js +389 -0
  46. package/lib/service/agent/capabilities.js +409 -0
  47. package/lib/service/{chat → agent/context}/ContextWindow.js +37 -12
  48. package/lib/service/{chat → agent/context}/ExplorationTracker.js +112 -33
  49. package/lib/service/{chat → agent/core}/ChatAgentPrompts.js +5 -3
  50. package/lib/service/agent/core/LoopContext.js +170 -0
  51. package/lib/service/agent/core/MessageAdapter.js +223 -0
  52. package/lib/service/agent/core/ToolExecutionPipeline.js +376 -0
  53. package/lib/service/{chat → agent/domain}/ChatAgentTasks.js +15 -98
  54. package/lib/service/{chat → agent/domain}/EpisodicConsolidator.js +7 -7
  55. package/lib/service/{chat → agent/domain}/EvidenceCollector.js +4 -2
  56. package/lib/service/{chat/AnalystAgent.js → agent/domain/insight-analyst.js} +37 -172
  57. package/lib/service/{chat/HandoffProtocol.js → agent/domain/insight-gate.js} +85 -135
  58. package/lib/service/agent/domain/insight-producer.js +270 -0
  59. package/lib/service/agent/domain/scan-prompts.js +444 -0
  60. package/lib/service/agent/forced-summary.js +266 -0
  61. package/lib/service/agent/index.js +91 -0
  62. package/lib/service/{chat → agent}/memory/ActiveContext.js +29 -1
  63. package/lib/service/{chat → agent}/memory/MemoryCoordinator.js +7 -7
  64. package/lib/service/{chat/ProjectSemanticMemory.js → agent/memory/PersistentMemory.js} +359 -89
  65. package/lib/service/{chat → agent}/memory/SessionStore.js +1 -1
  66. package/lib/service/{chat → agent}/memory/index.js +1 -1
  67. package/lib/service/agent/policies.js +442 -0
  68. package/lib/service/agent/presets.js +305 -0
  69. package/lib/service/agent/strategies.js +756 -0
  70. package/lib/service/{chat → agent/tools}/ToolRegistry.js +3 -3
  71. package/lib/service/agent/tools/ai-analysis.js +75 -0
  72. package/lib/service/{chat → agent}/tools/composite.js +2 -1
  73. package/lib/service/{chat → agent}/tools/guard.js +1 -121
  74. package/lib/service/{chat → agent}/tools/index.js +27 -21
  75. package/lib/service/{chat → agent}/tools/infrastructure.js +1 -1
  76. package/lib/service/agent/tools/knowledge-graph.js +112 -0
  77. package/lib/service/agent/tools/scan-recipe.js +189 -0
  78. package/lib/service/agent/tools/system-interaction.js +476 -0
  79. package/lib/service/automation/DirectiveDetector.js +0 -1
  80. package/lib/service/automation/FileWatcher.js +0 -8
  81. package/lib/service/automation/handlers/CreateHandler.js +7 -3
  82. package/lib/service/automation/handlers/DraftHandler.js +7 -6
  83. package/lib/service/module/ModuleService.js +40 -73
  84. package/lib/service/skills/SignalCollector.js +26 -19
  85. package/lib/service/snippet/codecs/VSCodeCodec.js +1 -1
  86. package/lib/shared/FieldSpec.js +1 -1
  87. package/lib/shared/StyleGuide.js +1 -1
  88. package/package.json +4 -1
  89. package/resources/native-ui/screenshot.swift +228 -0
  90. package/dashboard/dist/assets/index-D5jiDBQG.css +0 -1
  91. package/dashboard/dist/assets/index-e5OKj-Ni.js +0 -128
  92. package/lib/core/discovery/SpmDiscoverer.js +0 -5
  93. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +0 -750
  94. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +0 -277
  95. package/lib/http/routes/spm.js +0 -5
  96. package/lib/infrastructure/external/XcodeAutomation.js +0 -15
  97. package/lib/service/chat/ChatAgent.js +0 -1602
  98. package/lib/service/chat/Memory.js +0 -161
  99. package/lib/service/chat/ProducerAgent.js +0 -431
  100. package/lib/service/chat/ReasoningTrace.js +0 -523
  101. package/lib/service/chat/TaskPipeline.js +0 -357
  102. package/lib/service/chat/WorkingMemory.js +0 -359
  103. package/lib/service/chat/memory/PersistentMemory.js +0 -450
  104. package/lib/service/chat/tools/ai-analysis.js +0 -267
  105. package/lib/service/chat/tools/knowledge-graph.js +0 -234
  106. package/lib/service/chat/tools.js +0 -18
  107. package/lib/service/snippet/PlaceholderConverter.js +0 -5
  108. package/lib/service/snippet/codecs/XcodeCodec.js +0 -5
  109. /package/lib/service/{chat → agent}/ConversationStore.js +0 -0
  110. /package/lib/service/{chat → agent}/tools/_shared.js +0 -0
  111. /package/lib/service/{chat → agent}/tools/ast-graph.js +0 -0
  112. /package/lib/service/{chat → agent}/tools/lifecycle.js +0 -0
  113. /package/lib/service/{chat → agent}/tools/project-access.js +0 -0
  114. /package/lib/service/{chat → agent}/tools/query.js +0 -0
@@ -0,0 +1,1056 @@
1
+ /**
2
+ * AgentRuntime — 统一 Agent 执行引擎 (The Brain)
3
+ *
4
+ * 核心思想: 不存在类型分野,只有 ONE Runtime。
5
+ * 只有 ONE Runtime,由 Capability + Strategy + Policy 配置驱动。
6
+ *
7
+ * AgentRuntime 是:
8
+ * - ReAct 循环的宿主 (Thought → Action → Observation)
9
+ * - Capability 的组合容器 (加载哪些技能)
10
+ * - Policy 的执行者 (遵守哪些约束)
11
+ * - Strategy 的被委托者 (Strategy 调用 runtime.reactLoop())
12
+ *
13
+ * 认知架构 (CoALA):
14
+ * Perception → Working Memory → Reasoning → Action → Reflection
15
+ * │ │ │ │ │
16
+ * AgentMessage history+memory LLM call Tools Policy.validateAfter
17
+ *
18
+ * 引擎级能力:
19
+ * - ContextWindow: 三级递进压缩,动态 token 预算 (可选注入)
20
+ * - ExplorationTracker: 阶段状态机 + 信号收集 + Nudge + Graceful exit (可选注入)
21
+ * - AI 错误恢复: consecutiveAiErrors 2-strike → context reset → forced summary
22
+ * - 空响应重试: consecutiveEmptyResponses + rollback (system 场景)
23
+ * - 熔断器感知: _circuitState === 'OPEN' → 直接合成摘要
24
+ * - 工具调用数量限制: MAX_TOOL_CALLS_PER_ITER = 8
25
+ * - 提交去重: submittedTitles / submittedPatterns
26
+ * - cleanFinalAnswer: 去除 nudge 噪声
27
+ *
28
+ * @module AgentRuntime
29
+ */
30
+
31
+ import { randomUUID } from 'node:crypto';
32
+ import Logger from '../../infrastructure/logging/Logger.js';
33
+ import { AgentState, AgentPhase } from './AgentState.js';
34
+ import { AgentEventBus, AgentEvents } from './AgentEventBus.js';
35
+ import { PolicyEngine, BudgetPolicy, SafetyPolicy } from './policies.js';
36
+ import { CapabilityRegistry, Capability } from './capabilities.js';
37
+ import { limitToolResult } from './context/ContextWindow.js';
38
+ import { cleanFinalAnswer } from './core/ChatAgentPrompts.js';
39
+ import { produceForcedSummary } from './forced-summary.js';
40
+ import { createMessageAdapter } from './core/MessageAdapter.js';
41
+ import { LoopContext } from './core/LoopContext.js';
42
+ import { createToolPipeline } from './core/ToolExecutionPipeline.js';
43
+
44
+ /** 单次迭代允许的最大工具调用数 */
45
+ const MAX_TOOL_CALLS_PER_ITER = 8;
46
+
47
+ /**
48
+ * @typedef {Object} RuntimeConfig
49
+ * @property {string} [id] — 运行时唯一 ID
50
+ * @property {string} [presetName] — 使用的 Preset 名称 (仅标识)
51
+ * @property {import('../../external/ai/AiProvider.js').AiProvider} aiProvider — LLM 提供商
52
+ * @property {import('./tools/ToolRegistry.js').ToolRegistry} toolRegistry — 工具注册表
53
+ * @property {Object} [container] — DI 容器
54
+ * @property {Capability[]} capabilities — 已实例化的 Capability 列表
55
+ * @property {import('./strategies.js').Strategy} strategy — 执行策略
56
+ * @property {PolicyEngine} policies — 策略引擎
57
+ * @property {Object} [persona] — 人格配置
58
+ * @property {Object} [memory] — 记忆配置
59
+ * @property {Function} [onProgress] — 进度回调
60
+ * @property {Function} [onToolCall] — 工具调用通知钩子 (name, args, result, iteration)
61
+ * @property {string} [lang] — 语言偏好
62
+ * @property {string} [projectRoot] — 项目根目录 (传入工具上下文)
63
+ */
64
+
65
+ export class AgentRuntime {
66
+ /** @type {string} */
67
+ id;
68
+ /** @type {string} */
69
+ presetName;
70
+ /** @type {AgentState} */
71
+ state;
72
+ /** @type {AgentEventBus} */
73
+ bus;
74
+ /** @type {import('../../external/ai/AiProvider.js').AiProvider} */
75
+ aiProvider;
76
+ /** @type {import('./tools/ToolRegistry.js').ToolRegistry} */
77
+ toolRegistry;
78
+ /** @type {Object} */
79
+ container;
80
+ /** @type {Capability[]} */
81
+ capabilities;
82
+ /** @type {import('./strategies.js').Strategy} */
83
+ strategy;
84
+ /** @type {PolicyEngine} */
85
+ policies;
86
+ /** @type {Object} */
87
+ persona;
88
+ /** @type {Object} */
89
+ memoryConfig;
90
+ /** @type {Function|null} */
91
+ onProgress;
92
+ /** @type {string|null} */
93
+ lang;
94
+ /** @type {Logger} */
95
+ logger;
96
+ /** @type {string} */
97
+ #projectRoot;
98
+ /** @type {Array|null} 文件缓存 (bootstrap 场景注入) */
99
+ #fileCache = null;
100
+ /** @type {string[]} 额外工具白名单 (调用方按需注入,不经 Capability) */
101
+ #additionalTools = [];
102
+ /** @type {import('./core/ToolExecutionPipeline.js').ToolExecutionPipeline} */
103
+ #toolPipeline;
104
+
105
+ // ── 执行统计 ──
106
+ iterationCount = 0;
107
+ toolCallHistory = [];
108
+ tokenUsage = { input: 0, output: 0 };
109
+ startTime = 0;
110
+
111
+ /**
112
+ * @param {RuntimeConfig} config
113
+ */
114
+ constructor(config) {
115
+ this.id = config.id || `runtime_${randomUUID().slice(0, 8)}`;
116
+ this.presetName = config.presetName || 'custom';
117
+ this.aiProvider = config.aiProvider;
118
+ this.toolRegistry = config.toolRegistry;
119
+ this.container = config.container || null;
120
+ this.capabilities = config.capabilities || [];
121
+ this.strategy = config.strategy;
122
+ this.policies = config.policies || new PolicyEngine([]);
123
+ this.persona = config.persona || {};
124
+ this.memoryConfig = config.memory || {};
125
+ this.onProgress = config.onProgress || null;
126
+ this.onToolCall = config.onToolCall || null;
127
+ this.lang = config.lang || null;
128
+ this.logger = Logger.getInstance();
129
+ this.bus = AgentEventBus.getInstance();
130
+ this.#projectRoot = config.projectRoot || process.cwd();
131
+ this.#additionalTools = config.additionalTools || [];
132
+ this.#toolPipeline = createToolPipeline();
133
+
134
+ this.state = new AgentState({
135
+ initialData: { runtimeId: this.id, preset: this.presetName },
136
+ });
137
+
138
+ this.bus.publish(AgentEvents.AGENT_CREATED, {
139
+ agentId: this.id,
140
+ preset: this.presetName,
141
+ capabilities: this.capabilities.map(c => c.name),
142
+ strategy: this.strategy?.name,
143
+ }, { source: this.id });
144
+ }
145
+
146
+ // ─── 公共 API ─────────────────────────────────
147
+
148
+ /**
149
+ * 执行 Agent — 入口
150
+ *
151
+ * @param {import('./AgentMessage.js').AgentMessage} message — 统一消息
152
+ * @param {Object} [opts] — 策略特定选项 (如 FanOut 的 items)
153
+ * @returns {Promise<AgentResult>}
154
+ *
155
+ * @typedef {Object} AgentResult
156
+ * @property {string} reply — 最终文本回复
157
+ * @property {Array} toolCalls — 工具调用记录
158
+ * @property {Object} tokenUsage — Token 用量
159
+ * @property {number} iterations — 循环次数
160
+ * @property {number} durationMs — 执行耗时
161
+ * @property {Object} [phases] — Pipeline/FanOut 阶段详情
162
+ * @property {Object} state — 状态快照
163
+ */
164
+ async execute(message, opts = {}) {
165
+ this.startTime = Date.now();
166
+ this.iterationCount = 0;
167
+ this.toolCallHistory = [];
168
+ this.tokenUsage = { input: 0, output: 0 };
169
+
170
+ // ── Policy: 执行前校验 ──
171
+ const beforeCheck = this.policies.validateBefore({ message, capabilities: this.capabilities });
172
+ if (!beforeCheck.ok) {
173
+ this.logger.warn(`[AgentRuntime] Policy rejected: ${beforeCheck.reason}`);
174
+ return {
175
+ reply: `⚠️ ${beforeCheck.reason}`,
176
+ toolCalls: [],
177
+ tokenUsage: { input: 0, output: 0 },
178
+ iterations: 0,
179
+ durationMs: 0,
180
+ state: this.state.toJSON(),
181
+ };
182
+ }
183
+
184
+ // ── 超时保护 ──
185
+ const budget = this.policies.getBudget();
186
+ const timeoutMs = budget?.timeoutMs || 300_000;
187
+
188
+ let timeoutId;
189
+ const timeoutPromise = new Promise((_, reject) => {
190
+ timeoutId = setTimeout(() => reject(new Error(`Agent timeout after ${timeoutMs}ms`)), timeoutMs);
191
+ });
192
+
193
+ try {
194
+ // ── 委托给 Strategy ──
195
+ const resultPromise = this.strategy.execute(this, message, opts);
196
+ const result = await Promise.race([resultPromise, timeoutPromise]);
197
+ clearTimeout(timeoutId);
198
+
199
+ // ── Policy: 执行后校验 ──
200
+ const afterCheck = this.policies.validateAfter(result);
201
+ if (!afterCheck.ok) {
202
+ this.logger.warn(`[AgentRuntime] Quality check: ${afterCheck.reason}`);
203
+ result.qualityWarning = afterCheck.reason;
204
+ }
205
+
206
+ // 状态完成
207
+ this.#safeTransition('finish', { reply: result.reply?.slice(0, 100) });
208
+
209
+ // 回复给原始渠道
210
+ if (message.replyFn && result.reply) {
211
+ await message.reply(result.reply);
212
+ }
213
+
214
+ result.state = this.state.toJSON();
215
+ result.durationMs = Date.now() - this.startTime;
216
+
217
+ this.bus.publish(AgentEvents.AGENT_COMPLETED, {
218
+ agentId: this.id,
219
+ preset: this.presetName,
220
+ iterations: result.iterations,
221
+ durationMs: result.durationMs,
222
+ }, { source: this.id });
223
+
224
+ return result;
225
+
226
+ } catch (err) {
227
+ clearTimeout(timeoutId);
228
+ this.state.send('error', { error: err.message });
229
+ this.bus.publish(AgentEvents.AGENT_FAILED, {
230
+ agentId: this.id,
231
+ error: err.message,
232
+ }, { source: this.id });
233
+ throw err;
234
+ }
235
+ }
236
+
237
+ // ─── ReAct Loop — 供 Strategy 调用 ──────────
238
+
239
+ /**
240
+ * 核心 ReAct 循环。Strategy 调用此方法执行实际的 LLM + Tool 交互。
241
+ *
242
+ * 引擎级能力通过可选参数注入:
243
+ * - contextWindow → 三级递进压缩 + 动态工具结果限额
244
+ * - tracker → ExplorationTracker 阶段管理 + Nudge + Graceful exit
245
+ * - trace → ActiveContext 推理链记录
246
+ * - memoryCoordinator → 缓存/动态提示/观察记录
247
+ * - sharedState → 提交去重 { submittedTitles, submittedPatterns }
248
+ * - source → 'user' | 'system' (影响错误恢复 + 强制摘要行为)
249
+ *
250
+ * 向后兼容: 以上参数均为可选。不提供时退化为原始裸循环。
251
+ *
252
+ * @param {string} prompt — 用户/系统提示
253
+ * @param {Object} [opts]
254
+ * @param {Array} [opts.history] — 对话历史
255
+ * @param {Object} [opts.context] — 额外上下文
256
+ * @param {string[]} [opts.capabilityOverride] — 临时覆盖 capability (Pipeline 阶段用)
257
+ * @param {Object} [opts.budgetOverride] — 临时覆盖 budget
258
+ * @param {string} [opts.systemPromptOverride] — 完全覆盖系统提示词 (Bootstrap 阶段专用)
259
+ * @param {Function} [opts.onToolCall] — 本轮独立的工具调用钩子,优先于 runtime 级
260
+ * @param {import('./context/ContextWindow.js').ContextWindow} [opts.contextWindow] — 上下文窗口管理器
261
+ * @param {Object} [opts.tracker] — ExplorationTracker 实例
262
+ * @param {Object} [opts.trace] — ActiveContext 实例
263
+ * @param {Object} [opts.memoryCoordinator] — MemoryCoordinator 实例
264
+ * @param {Object} [opts.sharedState] — 共享状态 { submittedTitles, submittedPatterns }
265
+ * @param {string} [opts.source] — 'user' | 'system'
266
+ * @param {string} [opts.toolChoiceOverride] — 首轮 toolChoice 覆盖 ('required'/'auto'/'none')
267
+ * 首轮强制 LLM 生成 tool call(LLM 自行决定调哪个工具、传什么参数)。
268
+ * 这不是替 LLM 做决定,而是告诉 LLM "你必须调用某个工具"。
269
+ * 仅在第一轮生效,后续轮次恢复正常 toolChoice 逻辑。
270
+ * @returns {Promise<AgentResult>}
271
+ */
272
+ async reactLoop(prompt, opts = {}) {
273
+ const ctx = this.#initLoop(prompt, opts);
274
+
275
+ // ─── ReAct 主循环 (编排骨架) ─────
276
+ while (true) {
277
+ ctx.iteration++;
278
+ this.iterationCount++;
279
+
280
+ // ActiveContext: 开始新轮次 (必须在 #shouldExit 前, 保证 endRound 有配对)
281
+ ctx.trace?.startRound(ctx.iteration);
282
+
283
+ // 退出判定 (tracker + policy)
284
+ if (this.#shouldExit(ctx)) break;
285
+
286
+ // 迭代准备 (hooks + nudge + compact + toolChoice + prompt)
287
+ const { toolChoice, effectiveSystemPrompt, effectivePrompt } =
288
+ this.#prepareIteration(ctx);
289
+
290
+ // LLM 调用 (含错误恢复 + 空响应重试)
291
+ const llmResult = await this.#callLLM(ctx, toolChoice, effectiveSystemPrompt, effectivePrompt);
292
+ if (!llmResult) break;
293
+ if (llmResult.__continue) continue;
294
+
295
+ // ActiveContext: 记录 AI 的推理文本 + 提取/更新计划
296
+ if (ctx.trace && llmResult.text) {
297
+ ctx.trace.setThought(llmResult.text);
298
+ ctx.trace.extractAndSetPlan?.(llmResult.text, ctx.iteration);
299
+ }
300
+
301
+ // 分支: 有 Tool Call
302
+ if (llmResult.functionCalls?.length > 0) {
303
+ const exitAfterTools = await this.#processToolCalls(ctx, llmResult, effectiveSystemPrompt);
304
+ if (exitAfterTools) break;
305
+ continue;
306
+ }
307
+
308
+ // 分支: 纯文本回复
309
+ if (this.#processTextResponse(ctx, llmResult)) break;
310
+ }
311
+
312
+ return this.#finalize(ctx);
313
+ }
314
+
315
+ // ─── 提取方法: reactLoop 内部阶段 ────────────
316
+
317
+ /**
318
+ * 初始化循环上下文 — 封装 reactLoop 前 ~60 行初始化逻辑
319
+ * @param {string} prompt
320
+ * @param {Object} opts
321
+ * @returns {LoopContext}
322
+ */
323
+ #initLoop(prompt, opts) {
324
+ const {
325
+ history = [], context = {}, capabilityOverride, budgetOverride,
326
+ systemPromptOverride, onToolCall,
327
+ contextWindow, tracker, trace, memoryCoordinator, sharedState, source,
328
+ toolChoiceOverride,
329
+ } = opts;
330
+
331
+ // 解析 capabilities
332
+ const caps = capabilityOverride
333
+ ? this.#resolveCapabilities(capabilityOverride)
334
+ : this.capabilities;
335
+
336
+ // 构建基础系统提示词
337
+ let baseSystemPrompt = systemPromptOverride || this.#buildSystemPrompt(caps, context);
338
+
339
+ // 收集工具 (caps 为空数组时 = 明确无工具)
340
+ const allowedTools = this.#collectTools(caps);
341
+ const noToolsExplicit = Array.isArray(capabilityOverride) && capabilityOverride.length === 0;
342
+ const toolSchemas = noToolsExplicit
343
+ ? []
344
+ : this.toolRegistry.getToolSchemas(allowedTools.length > 0 ? allowedTools : null);
345
+
346
+ // 创建统一消息适配器 (消除 useCtxWin 双模式)
347
+ const messages = createMessageAdapter(contextWindow);
348
+
349
+ // 加载历史 + 用户 prompt
350
+ for (const h of history) {
351
+ if (h.role === 'assistant') {
352
+ messages.appendAssistantText(h.content);
353
+ } else {
354
+ messages.appendUserMessage(h.content);
355
+ }
356
+ }
357
+ messages.appendUserMessage(prompt);
358
+
359
+ // 预算
360
+ const budget = budgetOverride || this.policies.getBudget() || {
361
+ maxIterations: 20, maxTokens: 4096, temperature: 0.7,
362
+ };
363
+
364
+ // 系统源: 注入轮次预算 (先验锚定阶段节奏)
365
+ // 与旧版 ChatAgent 对齐: 60% 探索 → 80% 验证 → 最后 20% 输出总结
366
+ const isSystem = source === 'system';
367
+ if (isSystem && tracker && !baseSystemPrompt.includes('轮次预算')) {
368
+ const maxIter = budget.maxIterations || 24;
369
+ const exploreEnd = Math.floor(maxIter * 0.6);
370
+ const verifyEnd = Math.floor(maxIter * 0.8);
371
+ baseSystemPrompt += `\n\n## 轮次预算\n- 总轮次: **${maxIter} 轮**\n- 探索阶段: 第 1-${exploreEnd} 轮(搜索和结构化查询)\n- 验证阶段: 第 ${exploreEnd + 1}-${verifyEnd} 轮(读取关键文件确认细节)\n- 总结阶段: 第 ${verifyEnd + 1}-${maxIter} 轮(**停止工具调用,输出分析文本**)\n\n到达第 ${verifyEnd} 轮时你必须开始输出总结,不要继续搜索。`;
372
+ }
373
+
374
+ // 状态转移
375
+ this.#safeTransition('start', { prompt: prompt.slice(0, 100) });
376
+ this.#safeTransition('plan_ready');
377
+
378
+ this.bus.publish(AgentEvents.AGENT_STARTED, {
379
+ agentId: this.id,
380
+ prompt: prompt.slice(0, 100),
381
+ capabilities: caps.map(c => c.name),
382
+ }, { source: this.id });
383
+
384
+ return new LoopContext({
385
+ messages,
386
+ tracker: tracker || null,
387
+ trace: trace || null,
388
+ memoryCoordinator: memoryCoordinator || null,
389
+ sharedState: sharedState || null,
390
+ source: source || 'user',
391
+ budget,
392
+ capabilities: caps,
393
+ baseSystemPrompt,
394
+ toolSchemas,
395
+ prompt,
396
+ onToolCall: onToolCall || null,
397
+ context: context || {},
398
+ contextWindow: contextWindow || null,
399
+ toolChoiceOverride: toolChoiceOverride || null,
400
+ });
401
+ }
402
+
403
+ /**
404
+ * 退出判定 — 合并 tracker/policy 退出检查
405
+ * @param {LoopContext} ctx
406
+ * @returns {boolean} true = 应退出循环
407
+ */
408
+ #shouldExit(ctx) {
409
+ // ExplorationTracker: tick + 退出检查
410
+ if (ctx.tracker) {
411
+ ctx.tracker.tick();
412
+ if (ctx.tracker.shouldExit()) {
413
+ this.logger.info(
414
+ `[AgentRuntime] tracker exit: phase=${ctx.tracker.phase}, iter=${ctx.tracker.iteration}, submits=${ctx.tracker.totalSubmits}`
415
+ );
416
+ return true;
417
+ }
418
+ }
419
+
420
+ // Capability 前置钩子
421
+ for (const cap of ctx.capabilities) {
422
+ cap.onBeforeStep({
423
+ iteration: ctx.iteration,
424
+ messages: ctx.messages.toMessages(),
425
+ prompt: ctx.prompt,
426
+ });
427
+ }
428
+
429
+ // ── Per-stage budget timeout (从 budgetOverride 注入) ──
430
+ // 与 BudgetPolicy 的全局 timeoutMs (600s) 不同,此处使用阶段级 budget.timeoutMs
431
+ // 保证 Analyst/Producer 各自有独立的超时边界,避免一个阶段消耗完所有时长
432
+ if (ctx.budget?.timeoutMs && Date.now() - ctx.loopStartTime > ctx.budget.timeoutMs) {
433
+ this.logger.info(
434
+ `[AgentRuntime] ⏰ Stage budget timeout: ${ctx.budget.timeoutMs}ms exceeded (elapsed: ${Date.now() - ctx.loopStartTime}ms)`,
435
+ );
436
+ return true;
437
+ }
438
+
439
+ // Policy 实时校验
440
+ // 当 ExplorationTracker 存在时,由 tracker 自己管理 maxIterations + grace 轮次,
441
+ // 跳过 BudgetPolicy 的 iteration 限制,避免竞争(tracker 给了 grace 但 policy 立即杀掉循环)。
442
+ // tracker 内部有硬上限 (maxIterations + 2) 保证不会无限循环。
443
+ const skipPolicyIterCheck = !!ctx.tracker;
444
+ const duringCheck = this.policies.validateDuring({
445
+ iteration: skipPolicyIterCheck ? 0 : ctx.iteration, // tracker 模式下用 0 绕过 iteration 检查
446
+ toolCalls: this.toolCallHistory,
447
+ tokenUsage: this.tokenUsage,
448
+ startTime: ctx.loopStartTime,
449
+ });
450
+ if (!duringCheck.ok) {
451
+ this.logger.info(`[AgentRuntime] Policy stop: ${duringCheck.reason}`);
452
+ return true;
453
+ }
454
+
455
+ return false;
456
+ }
457
+
458
+ /**
459
+ * 迭代准备 — 合并 nudge/压缩/提示词增强/toolChoice
460
+ * @param {LoopContext} ctx
461
+ * @returns {{ toolChoice: string, effectiveSystemPrompt: string, effectivePrompt: string }}
462
+ */
463
+ #prepareIteration(ctx) {
464
+ const { tracker, trace, capabilities, messages, prompt } = ctx;
465
+ const maxIterations = ctx.maxIterations;
466
+
467
+ this.#emitProgress('thinking', { iteration: ctx.iteration, maxIterations });
468
+
469
+ // Nudge 注入 (ExplorationTracker)
470
+ if (tracker) {
471
+ const nudge = tracker.getNudge(trace);
472
+ if (nudge) {
473
+ messages.appendUserNudge(nudge.text);
474
+ this.logger.info(
475
+ `[AgentRuntime] 💬 injected ${nudge.type} nudge at iter ${ctx.iteration}`
476
+ );
477
+ const _dim = ctx.sharedState?._dimensionMeta?.id || '';
478
+ console.log(`\n\x1b[36m━━━ Nudge [${nudge.type}] iter=${ctx.iteration}${_dim ? ` dim=${_dim}` : ''} ━━━\x1b[0m`);
479
+ console.log(`\x1b[33m${nudge.text}\x1b[0m\n`);
480
+ }
481
+ }
482
+
483
+ // 压缩检查
484
+ const compactResult = messages.compactIfNeeded();
485
+ if (compactResult.level > 0) {
486
+ this.logger.info(
487
+ `[AgentRuntime] context compacted: L${compactResult.level}, removed ${compactResult.removed} items`
488
+ );
489
+ }
490
+
491
+ // 动态 toolChoice
492
+ const forceSummaryAt = Math.max(2, Math.ceil(maxIterations * 0.8));
493
+ const forceSummary = !tracker && ctx.iteration >= forceSummaryAt;
494
+ let toolChoice;
495
+ if (ctx.toolChoiceOverride && ctx.iteration === 1) {
496
+ // 首轮 toolChoice 覆盖: 强制 LLM 生成 tool call (LLM 自行决定调哪个、传什么)
497
+ toolChoice = ctx.toolChoiceOverride;
498
+ } else if (tracker) {
499
+ toolChoice = tracker.getToolChoice();
500
+ } else {
501
+ toolChoice = ctx.toolSchemas.length > 0
502
+ ? (forceSummary ? 'none' : 'auto')
503
+ : 'none';
504
+ }
505
+
506
+ // 系统提示词增强 (阶段上下文 + 动态记忆)
507
+ let effectiveSystemPrompt = ctx.baseSystemPrompt;
508
+ if (tracker) {
509
+ effectiveSystemPrompt += tracker.getPhaseContext();
510
+ } else if (ctx.isSystem && !tracker) {
511
+ const remaining = maxIterations - ctx.iteration;
512
+ effectiveSystemPrompt += `\n\n## 当前进度\n第 ${ctx.iteration}/${maxIterations} 轮 | 剩余 ${remaining} 轮`;
513
+ }
514
+ if (ctx.isSystem && ctx.memoryCoordinator) {
515
+ const wmContext = ctx.memoryCoordinator.buildDynamicMemoryPrompt?.({
516
+ mode: ctx.source || 'analyst',
517
+ scopeId: ctx.context?.dimensionScopeId || null,
518
+ });
519
+ if (wmContext) effectiveSystemPrompt += `\n\n${wmContext}`;
520
+ }
521
+
522
+ // 非 tracker 模式的强制摘要提示注入
523
+ const effectivePrompt = forceSummary
524
+ ? `${prompt}\n\n[系统提示] 已进入最后阶段,请停止调用工具,基于已有信息输出总结。`
525
+ : prompt;
526
+
527
+ return { toolChoice, effectiveSystemPrompt, effectivePrompt };
528
+ }
529
+
530
+ /**
531
+ * LLM 调用 — 含错误恢复 + 空响应重试
532
+ *
533
+ * @param {LoopContext} ctx
534
+ * @param {string} toolChoice
535
+ * @param {string} effectiveSystemPrompt
536
+ * @param {string} effectivePrompt
537
+ * @returns {Promise<Object|null>} llmResult 或 null (表示应退出)
538
+ */
539
+ async #callLLM(ctx, toolChoice, effectiveSystemPrompt, effectivePrompt) {
540
+ this.bus.publish(AgentEvents.LLM_CALL_START, {
541
+ agentId: this.id,
542
+ iteration: ctx.iteration,
543
+ }, { source: this.id });
544
+
545
+ let llmResult;
546
+ try {
547
+ // toolChoice='none' 时不发送 toolSchemas —— 部分 LLM (Gemini) 在看到
548
+ // 工具定义但被禁止调用时会返回空内容,导致 SUMMARIZE 阶段失败
549
+ const effectiveToolSchemas = toolChoice === 'none'
550
+ ? undefined
551
+ : (ctx.toolSchemas.length > 0 ? ctx.toolSchemas : undefined);
552
+ llmResult = await this.aiProvider.chatWithTools(effectivePrompt, {
553
+ messages: ctx.messages.toMessages(),
554
+ toolSchemas: effectiveToolSchemas,
555
+ toolChoice: effectiveToolSchemas ? toolChoice : undefined,
556
+ systemPrompt: effectiveSystemPrompt,
557
+ temperature: ctx.budget.temperature ?? (ctx.isSystem ? 0.3 : 0.7),
558
+ maxTokens: ctx.budget.maxTokens ?? (ctx.isSystem ? 8192 : 4096),
559
+ });
560
+ ctx.consecutiveAiErrors = 0;
561
+ } catch (aiErr) {
562
+ return this.#handleAiError(ctx, aiErr);
563
+ }
564
+
565
+ // 累计 Token (runtime 级 + loop 级)
566
+ if (llmResult.usage) {
567
+ this.tokenUsage.input += (llmResult.usage.inputTokens || 0);
568
+ this.tokenUsage.output += (llmResult.usage.outputTokens || 0);
569
+ ctx.addTokenUsage(llmResult.usage);
570
+ }
571
+
572
+ this.bus.publish(AgentEvents.LLM_CALL_END, {
573
+ agentId: this.id,
574
+ hasToolCalls: !!llmResult.functionCalls?.length,
575
+ hasText: !!llmResult.text,
576
+ usage: llmResult.usage,
577
+ }, { source: this.id });
578
+
579
+ // 空响应重试
580
+ if (!llmResult.text && !llmResult.functionCalls?.length) {
581
+ // B4 fix: SUMMARIZE 阶段也允许重试 — force_exit nudge 刚注入时 LLM 可能
582
+ // 需要额外一轮才能生成有效输出。与 ExplorationTracker 的 2 轮 grace 对齐,
583
+ // 避免 grace 机制被架空。重试次数由 tracker.phaseRounds 控制而非独立计数。
584
+ const isTerminal = ctx.tracker && (ctx.tracker.phase === 'SUMMARIZE');
585
+ if (isTerminal) {
586
+ const phaseRounds = ctx.tracker.metrics?.phaseRounds ?? 0;
587
+ if (phaseRounds < 2) {
588
+ ctx.consecutiveEmptyResponses++;
589
+ this.logger.warn(
590
+ `[AgentRuntime] ⚠ empty response in SUMMARIZE — retrying (grace ${phaseRounds + 1}/2)`
591
+ );
592
+ // 不 rollbackTick: 让 tracker 计入 phaseRounds 以便到达 grace 上限退出
593
+ await new Promise(r => setTimeout(r, 1500));
594
+ return { __continue: true };
595
+ }
596
+ this.logger.warn('[AgentRuntime] ⚠ empty response in SUMMARIZE (grace exhausted) — proceeding to forced summary');
597
+ return null;
598
+ }
599
+ if (ctx.isSystem && ctx.consecutiveEmptyResponses < 2) {
600
+ ctx.consecutiveEmptyResponses++;
601
+ this.logger.warn(
602
+ `[AgentRuntime] ⚠ empty response — retrying (${ctx.consecutiveEmptyResponses}/2)`
603
+ );
604
+ ctx.tracker?.rollbackTick?.();
605
+ await new Promise(r => setTimeout(r, 1500));
606
+ // 返回 'continue' 信号 — 调用方需重走循环
607
+ return { __continue: true };
608
+ }
609
+ return null; // 退出
610
+ }
611
+ if (llmResult.text || llmResult.functionCalls?.length) {
612
+ ctx.consecutiveEmptyResponses = 0;
613
+ }
614
+
615
+ // Graceful exit 保护
616
+ if (ctx.tracker?.isGracefulExit && llmResult.functionCalls?.length > 0) {
617
+ this.logger.warn(
618
+ `[AgentRuntime] ⚠ AI returned ${llmResult.functionCalls.length} tool calls despite toolChoice=none (graceful exit) — ignoring`
619
+ );
620
+ if (llmResult.text) {
621
+ ctx.lastReply = cleanFinalAnswer(llmResult.text);
622
+ return null; // 退出
623
+ }
624
+ return { __continue: true };
625
+ }
626
+
627
+ return llmResult;
628
+ }
629
+
630
+ /**
631
+ * AI 错误处理 — 熔断器感知 + 2-strike 策略
632
+ * @returns {Object|null} — { __continue: true } 或 null (退出)
633
+ */
634
+ async #handleAiError(ctx, aiErr) {
635
+ ctx.consecutiveAiErrors++;
636
+ this.logger.warn(
637
+ `[AgentRuntime] AI call failed (attempt ${ctx.consecutiveAiErrors}): ${aiErr.message}`
638
+ );
639
+
640
+ ctx.tracker?.rollbackTick?.();
641
+
642
+ // 熔断器感知
643
+ if (aiErr.code === 'CIRCUIT_OPEN') {
644
+ this.logger.warn('[AgentRuntime] 🛑 circuit breaker OPEN — breaking to summary');
645
+ if (!ctx.isSystem) {
646
+ ctx.lastReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
647
+ }
648
+ return null;
649
+ }
650
+
651
+ // 2-strike 策略
652
+ if (ctx.consecutiveAiErrors >= 2) {
653
+ this.logger.warn('[AgentRuntime] 🛑 2 consecutive AI errors — breaking to summary');
654
+ ctx.messages.resetToPromptOnly();
655
+ if (!ctx.isSystem) {
656
+ ctx.lastReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
657
+ }
658
+ return null;
659
+ }
660
+
661
+ await new Promise(r => setTimeout(r, 2000));
662
+ return { __continue: true };
663
+ }
664
+
665
+ /**
666
+ * 工具调用处理 — 执行 + 记录 + 去重 + 阶段转换
667
+ *
668
+ * @param {LoopContext} ctx
669
+ * @param {Object} llmResult
670
+ * @param {string} effectiveSystemPrompt — 用于 budget 耗尽时的摘要调用
671
+ * @returns {Promise<boolean>} true = 应退出循环
672
+ */
673
+ async #processToolCalls(ctx, llmResult, effectiveSystemPrompt) {
674
+ const { tracker, trace, messages } = ctx;
675
+
676
+ // 工具调用数量限制
677
+ let activeCalls = llmResult.functionCalls;
678
+ if (activeCalls.length > MAX_TOOL_CALLS_PER_ITER) {
679
+ this.logger.warn(
680
+ `[AgentRuntime] ⚠ ${activeCalls.length} tool calls, capping to ${MAX_TOOL_CALLS_PER_ITER}`
681
+ );
682
+ tracker?.recordTruncatedCalls?.(activeCalls.length - MAX_TOOL_CALLS_PER_ITER);
683
+ activeCalls = activeCalls.slice(0, MAX_TOOL_CALLS_PER_ITER);
684
+ }
685
+
686
+ // 追加 assistant 消息
687
+ messages.appendAssistantWithToolCalls(llmResult.text || null, activeCalls);
688
+
689
+ let roundSubmitCount = 0;
690
+ let roundHasNewInfo = false;
691
+ const roundToolNames = [];
692
+
693
+ // 执行每个工具
694
+ for (const fc of activeCalls) {
695
+ this.#emitProgress('tool_call', { tool: fc.name, args: fc.args });
696
+
697
+ this.bus.publish(AgentEvents.TOOL_CALL_START, {
698
+ agentId: this.id,
699
+ tool: fc.name,
700
+ }, { source: this.id });
701
+
702
+ // 通过 Pipeline 执行 (safety → cache → execute → observe → track → trace → dedup)
703
+ const { result: toolResult, metadata } = await this.#toolPipeline.execute(fc, {
704
+ runtime: this,
705
+ loopCtx: ctx,
706
+ iteration: ctx.iteration,
707
+ });
708
+
709
+ const durationMs = metadata.durationMs;
710
+ const toolEntry = { tool: fc.name, args: fc.args, result: toolResult, durationMs };
711
+ ctx.toolCalls.push(toolEntry);
712
+ this.toolCallHistory.push(toolEntry);
713
+
714
+ if (metadata.isNew) roundHasNewInfo = true;
715
+ roundToolNames.push(fc.name);
716
+
717
+ // onToolCall 通知
718
+ const effectiveHook = ctx.onToolCall || this.onToolCall;
719
+ if (effectiveHook) {
720
+ try { effectiveHook(fc.name, fc.args, toolResult, ctx.iteration); } catch { /* 观察者错误不中断 */ }
721
+ }
722
+
723
+ this.bus.publish(AgentEvents.TOOL_CALL_END, {
724
+ agentId: this.id,
725
+ tool: fc.name,
726
+ durationMs,
727
+ success: !toolResult?.error,
728
+ }, { source: this.id });
729
+
730
+ // 工具结果格式化 (统一通过 MessageAdapter)
731
+ let resultStr = messages.formatToolResult(fc.name, toolResult);
732
+
733
+ // 提交去重: pipeline 中间件已标记 metadata
734
+ if (metadata.dedupMessage) {
735
+ resultStr = metadata.dedupMessage;
736
+ } else if (metadata.isSubmit) {
737
+ roundSubmitCount++;
738
+ }
739
+
740
+ // 进度回调 (tool_end 需要 resultStr.length)
741
+ this.#emitProgress('tool_end', {
742
+ tool: fc.name,
743
+ duration: durationMs,
744
+ status: toolResult?.error ? 'error' : 'ok',
745
+ error: toolResult?.error || undefined,
746
+ resultSize: resultStr.length,
747
+ });
748
+
749
+ // 追加 tool result
750
+ messages.appendToolResult(fc.id, fc.name, resultStr);
751
+ }
752
+
753
+ // ExplorationTracker: endRound → 检查阶段转换
754
+ if (tracker) {
755
+ tracker.updatePlanProgress?.(trace);
756
+ const transitionNudge = tracker.endRound({
757
+ hasNewInfo: roundHasNewInfo,
758
+ submitCount: roundSubmitCount,
759
+ toolNames: roundToolNames,
760
+ });
761
+ if (transitionNudge) {
762
+ messages.appendUserNudge(transitionNudge.text);
763
+ this.logger.info(
764
+ `[AgentRuntime] 📝 injected ${transitionNudge.type} nudge (${tracker.phase})`
765
+ );
766
+ const _dimT = ctx.sharedState?._dimensionMeta?.id || '';
767
+ console.log(`\n\x1b[35m━━━ Transition Nudge [${transitionNudge.type}] phase=${tracker.phase}${_dimT ? ` dim=${_dimT}` : ''} ━━━\x1b[0m`);
768
+ console.log(`\x1b[33m${transitionNudge.text}\x1b[0m\n`);
769
+ }
770
+ }
771
+
772
+ // ActiveContext: 关闭轮次
773
+ if (trace) {
774
+ trace.setRoundSummary?.({
775
+ newInfoCount: roundHasNewInfo ? 1 : 0,
776
+ totalCalls: activeCalls.length,
777
+ submits: roundSubmitCount,
778
+ cumulativeFiles: tracker?.getMetrics?.()?.uniqueFiles || 0,
779
+ cumulativePatterns: tracker?.getMetrics?.()?.uniquePatterns || 0,
780
+ });
781
+ trace.endRound?.();
782
+ }
783
+
784
+ // Capability 后置钩子
785
+ const stepToolEntries = ctx.toolCalls.slice(-activeCalls.length);
786
+ const stepResult = {
787
+ type: 'tool_calls',
788
+ toolCalls: stepToolEntries,
789
+ iteration: ctx.iteration,
790
+ };
791
+ for (const cap of ctx.capabilities) {
792
+ cap.onAfterStep(stepResult);
793
+ }
794
+
795
+ this.#safeTransition('step_done', stepResult);
796
+
797
+ // 检查预算 (非 tracker 模式)
798
+ if (!tracker && ctx.iteration >= ctx.maxIterations) {
799
+ const summary = await this.aiProvider.chatWithTools(ctx.prompt, {
800
+ messages: messages.toMessages(),
801
+ systemPrompt: effectiveSystemPrompt,
802
+ toolChoice: 'none',
803
+ temperature: ctx.budget.temperature ?? 0.7,
804
+ maxTokens: ctx.budget.maxTokens ?? 4096,
805
+ });
806
+ if (summary.usage) {
807
+ this.tokenUsage.input += (summary.usage.inputTokens || 0);
808
+ this.tokenUsage.output += (summary.usage.outputTokens || 0);
809
+ ctx.addTokenUsage(summary.usage);
810
+ }
811
+ ctx.lastReply = cleanFinalAnswer(summary.text || '');
812
+ return true; // 退出
813
+ }
814
+
815
+ this.#safeTransition('continue');
816
+ return false; // 继续循环
817
+ }
818
+
819
+ /**
820
+ * 文本响应处理 — tracker 阶段路由 + 非 tracker 直接终止
821
+ *
822
+ * @param {LoopContext} ctx
823
+ * @param {Object} llmResult
824
+ * @returns {boolean} true = 应退出循环
825
+ */
826
+ #processTextResponse(ctx, llmResult) {
827
+ const { tracker, trace, messages } = ctx;
828
+
829
+ if (tracker) {
830
+ // (setThought + extractAndSetPlan 已在主循环中统一处理)
831
+
832
+ const textResult = tracker.onTextResponse();
833
+
834
+ if (textResult.isFinalAnswer) {
835
+ ctx.lastReply = cleanFinalAnswer(llmResult.text || '');
836
+ this.logger.info(
837
+ `[AgentRuntime] ✅ final answer — ${ctx.lastReply.length} chars, ${tracker.iteration} iters, ${ctx.toolCalls.length} tool calls`
838
+ );
839
+ trace?.endRound?.();
840
+ return true;
841
+ }
842
+
843
+ if (textResult.needsDigestNudge) {
844
+ messages.appendAssistantText(llmResult.text || '');
845
+ messages.appendUserNudge(textResult.nudge);
846
+ this.logger.info('[AgentRuntime] 📝 injected SUMMARIZE nudge (text-triggered transition)');
847
+ const _dimD = ctx.sharedState?._dimensionMeta?.id || '';
848
+ console.log(`\n\x1b[34m━━━ Digest Nudge [SUMMARIZE]${_dimD ? ` dim=${_dimD}` : ''} ━━━\x1b[0m`);
849
+ console.log(`\x1b[33m${textResult.nudge}\x1b[0m\n`);
850
+ trace?.endRound?.();
851
+ return false; // continue
852
+ }
853
+
854
+ if (textResult.shouldContinue) {
855
+ messages.appendAssistantText(llmResult.text || '');
856
+ if (textResult.nudge) {
857
+ messages.appendUserNudge(textResult.nudge);
858
+ const _dimC = ctx.sharedState?._dimensionMeta?.id || '';
859
+ console.log(`\n\x1b[32m━━━ Continue Nudge${_dimC ? ` dim=${_dimC}` : ''} ━━━\x1b[0m`);
860
+ console.log(`\x1b[33m${textResult.nudge}\x1b[0m\n`);
861
+ }
862
+ trace?.endRound?.();
863
+ return false; // continue
864
+ }
865
+ }
866
+
867
+ // 非 tracker 模式: 文字回答即最终回答
868
+ ctx.lastReply = cleanFinalAnswer(llmResult.text || '');
869
+ trace?.endRound?.();
870
+ return true;
871
+ }
872
+
873
+ /**
874
+ * 循环退出后处理 — 强制摘要 + 构建返回值
875
+ * @param {LoopContext} ctx
876
+ * @returns {Promise<Object>}
877
+ */
878
+ async #finalize(ctx) {
879
+ // Scan pipeline: 所有结果在 toolCalls 中 (collect_scan_recipe),不需要文本回复
880
+ // 直接跳过 forced summary,避免浪费一次 LLM 调用
881
+ if (!ctx.lastReply && ctx.tracker?.submitToolName === 'collect_scan_recipe') {
882
+ const recipeCount = ctx.toolCalls.filter(tc => (tc.tool || tc.name) === 'collect_scan_recipe').length;
883
+ ctx.lastReply = `[scan complete: ${recipeCount} recipes collected]`;
884
+ }
885
+
886
+ // 强制摘要 (系统场景或 tracker 场景)
887
+ if (!ctx.lastReply && (ctx.tracker || ctx.isSystem)) {
888
+ const forcedResult = await produceForcedSummary({
889
+ aiProvider: this.aiProvider,
890
+ source: ctx.source,
891
+ toolCalls: ctx.toolCalls,
892
+ tracker: ctx.tracker,
893
+ contextWindow: ctx.contextWindow,
894
+ prompt: ctx.prompt,
895
+ tokenUsage: this.tokenUsage,
896
+ });
897
+ ctx.lastReply = forcedResult.reply;
898
+ if (forcedResult.tokenUsage) {
899
+ this.tokenUsage.input += (forcedResult.tokenUsage.input || 0);
900
+ this.tokenUsage.output += (forcedResult.tokenUsage.output || 0);
901
+ ctx.addTokenUsage({
902
+ inputTokens: forcedResult.tokenUsage.input || 0,
903
+ outputTokens: forcedResult.tokenUsage.output || 0,
904
+ });
905
+ }
906
+ }
907
+
908
+ return ctx.buildResult();
909
+ }
910
+
911
+ // ─── 公共工具方法 ────────────────────────────
912
+
913
+ /**
914
+ * 中止执行
915
+ */
916
+ abort(reason = 'User aborted') {
917
+ this.#safeTransition('abort', { reason });
918
+ this.bus.publish(AgentEvents.AGENT_ABORTED, {
919
+ agentId: this.id,
920
+ reason,
921
+ }, { source: this.id });
922
+ }
923
+
924
+ /**
925
+ * 注入内存文件缓存(bootstrap 场景: allFiles 已在内存中,避免重复磁盘读取)
926
+ * @param {Array|null} files — [{ relativePath, content, name }]
927
+ */
928
+ setFileCache(files) {
929
+ this.#fileCache = files;
930
+ }
931
+
932
+ /** 项目根目录 (供 ToolExecutionPipeline 等访问) */
933
+ get projectRoot() {
934
+ return this.#projectRoot;
935
+ }
936
+
937
+ /** 文件缓存 (供 ToolExecutionPipeline 等访问) */
938
+ get fileCache() {
939
+ return this.#fileCache;
940
+ }
941
+
942
+ /**
943
+ * 发送进度事件 (公开方法,供 ToolExecutionPipeline 中间件调用)
944
+ */
945
+ emitProgress(type, data = {}) {
946
+ this.#emitProgress(type, data);
947
+ }
948
+
949
+ // ─── 私有方法 ────────────────────────────────
950
+
951
+ /**
952
+ * 安全状态转移 — 忽略不合法转移而不是抛异常。
953
+ *
954
+ * Pipeline/FanOut 场景下 reactLoop() 被多次调用,
955
+ * 第 2+ 次调用时状态已不在 IDLE,直接 send('start') 会抛错。
956
+ * 此方法在转移不合法时静默跳过,保证多阶段执行不中断。
957
+ */
958
+ #safeTransition(event, payload = {}) {
959
+ try {
960
+ this.state.send(event, payload);
961
+ } catch {
962
+ // 转移不合法 — 在多阶段场景中这是预期行为,静默跳过
963
+ }
964
+ }
965
+
966
+ /**
967
+ * 构建系统提示词 — 从 Persona + Capabilities 组装
968
+ */
969
+ #buildSystemPrompt(caps, context) {
970
+ const parts = [];
971
+
972
+ // Persona (角色定义)
973
+ if (this.persona?.description) {
974
+ parts.push(`# 角色\n${this.persona.description}`);
975
+ }
976
+
977
+ // fileCache 文件清单 — 让 LLM 知道有哪些文件可用于工具调用
978
+ if (this.#fileCache && this.#fileCache.length > 0) {
979
+ const fileList = this.#fileCache.map(f => {
980
+ const lines = f.content ? f.content.split('\n').length : 0;
981
+ const name = f.name || f.relativePath || 'unknown';
982
+ return `- ${name} (${lines} 行${f.language ? ', ' + f.language : ''})`;
983
+ }).join('\n');
984
+ parts.push(`## 预加载文件\n以下文件已加载到缓存中,工具可通过 filePath 参数引用:\n${fileList}`);
985
+ }
986
+
987
+ // Capability prompt fragments
988
+ for (const cap of caps) {
989
+ parts.push(cap.promptFragment);
990
+
991
+ // Capability 动态上下文 (记忆、项目概况等)
992
+ const dynamicCtx = cap.buildContext({
993
+ ...context,
994
+ lang: this.lang,
995
+ memoryMode: this.memoryConfig?.mode,
996
+ });
997
+ if (dynamicCtx) parts.push(dynamicCtx);
998
+ }
999
+
1000
+ // 语言要求
1001
+ if (this.lang === 'en') {
1002
+ parts.push('\n## Language\nRespond in English.');
1003
+ } else if (this.lang === 'zh') {
1004
+ parts.push('\n## 语言\n用中文回复。代码/字段名保持英文。');
1005
+ }
1006
+
1007
+ return parts.join('\n\n');
1008
+ }
1009
+
1010
+ /**
1011
+ * 收集所有 Capability 的工具白名单
1012
+ * 如果任一 Capability tools 为空数组, 返回空 (使用全部工具)
1013
+ */
1014
+ #collectTools(caps) {
1015
+ const toolSet = new Set();
1016
+ let hasUnlimited = false;
1017
+ for (const cap of caps) {
1018
+ const tools = cap.tools;
1019
+ if (!tools || tools.length === 0) {
1020
+ hasUnlimited = true;
1021
+ break;
1022
+ }
1023
+ for (const t of tools) toolSet.add(t);
1024
+ }
1025
+ // 合并调用方按需注入的额外工具 (不经 Capability,避免污染共享能力)
1026
+ for (const t of this.#additionalTools) toolSet.add(t);
1027
+ return hasUnlimited ? [] : [...toolSet];
1028
+ }
1029
+
1030
+ /**
1031
+ * 解析 capability 名称为实例 (Pipeline 阶段覆盖时调用)
1032
+ */
1033
+ #resolveCapabilities(capNames) {
1034
+ if (capNames == null) return this.capabilities;
1035
+ if (capNames.length === 0) return []; // explicit empty = no tools
1036
+ return capNames.map(name => {
1037
+ if (typeof name === 'object' && name instanceof Capability) return name;
1038
+ // 先在已加载的 capabilities 中查找
1039
+ const existing = this.capabilities.find(c => c.name === name);
1040
+ if (existing) return existing;
1041
+ // 否则从注册表创建
1042
+ return CapabilityRegistry.create(name);
1043
+ });
1044
+ }
1045
+
1046
+ /**
1047
+ * 发送进度事件
1048
+ */
1049
+ #emitProgress(type, data = {}) {
1050
+ const event = { type, agentId: this.id, preset: this.presetName, ...data, timestamp: Date.now() };
1051
+ if (this.onProgress) this.onProgress(event);
1052
+ this.bus.publish(AgentEvents.PROGRESS, event, { source: this.id });
1053
+ }
1054
+ }
1055
+
1056
+ export default AgentRuntime;