autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
890 lines (889 loc) • 40.5 kB
JavaScript
/**
* AgentRuntime — 统一 Agent 执行引擎 (The Brain)
*
* 核心思想: 不存在类型分野,只有 ONE Runtime。
* 只有 ONE Runtime,由 Capability + Strategy + Policy 配置驱动。
*
* AgentRuntime 是:
* - ReAct 循环的宿主 (Thought → Action → Observation)
* - Capability 的组合容器 (加载哪些技能)
* - Policy 的执行者 (遵守哪些约束)
* - Strategy 的被委托者 (Strategy 调用 runtime.reactLoop())
*
* 认知架构 (CoALA):
* Perception → Working Memory → Reasoning → Action → Reflection
* │ │ │ │ │
* AgentMessage history+memory LLM call Tools Policy.validateAfter
*
* 引擎级能力:
* - ContextWindow: 三级递进压缩,动态 token 预算 (可选注入)
* - ExplorationTracker: 阶段状态机 + 信号收集 + Nudge + Graceful exit (可选注入)
* - AI 错误恢复: consecutiveAiErrors 2-strike → context reset → forced summary
* - 空响应重试: consecutiveEmptyResponses + rollback (system 场景)
* - 熔断器感知: _circuitState === 'OPEN' → 直接合成摘要
* - 工具调用数量限制: MAX_TOOL_CALLS_PER_ITER = 8
* - 提交去重: submittedTitles / submittedPatterns
* - cleanFinalAnswer: 去除 nudge 噪声
*
* @module AgentRuntime
*/
import { randomUUID } from 'node:crypto';
import Logger from '#infra/logging/Logger.js';
import { AgentEventBus, AgentEvents } from './AgentEventBus.js';
import { MAX_TOOL_CALLS_PER_ITER, } from './AgentRuntimeTypes.js';
import { AgentState } from './AgentState.js';
import { Capability, CapabilityRegistry } from './capabilities.js';
import { cleanFinalAnswer } from './core/ChatAgentPrompts.js';
import { continueResult, LLMResultType } from './core/LLMResultType.js';
import { LoopContext } from './core/LoopContext.js';
import { createMessageAdapter } from './core/MessageAdapter.js';
import { SystemPromptBuilder } from './core/SystemPromptBuilder.js';
import { createToolPipeline } from './core/ToolExecutionPipeline.js';
import { produceForcedSummary } from './forced-summary.js';
import { PolicyEngine } from './policies.js';
export { MAX_TOOL_CALLS_PER_ITER } from './AgentRuntimeTypes.js';
export class AgentRuntime {
onToolCall;
id;
presetName;
state;
bus;
aiProvider;
toolRegistry;
container;
capabilities;
strategy;
policies;
persona;
memoryConfig;
onProgress;
lang;
logger;
#projectRoot;
/** 文件缓存 (bootstrap 场景注入) */
#fileCache = null;
/** 额外工具白名单 (调用方按需注入,不经 Capability) */
#additionalTools = [];
#toolPipeline;
#promptBuilder;
// ── 执行统计 ──
iterationCount = 0;
toolCallHistory = [];
tokenUsage = { input: 0, output: 0 };
startTime = 0;
constructor(config) {
this.id = config.id || `runtime_${randomUUID().slice(0, 8)}`;
this.presetName = config.presetName || 'custom';
this.aiProvider = config.aiProvider;
this.toolRegistry = config.toolRegistry;
this.container = config.container || null;
this.capabilities = config.capabilities || [];
this.strategy = config.strategy;
this.policies = config.policies || new PolicyEngine([]);
this.persona = config.persona || {};
this.memoryConfig = config.memory || {};
this.onProgress = config.onProgress || null;
this.onToolCall = config.onToolCall || null;
this.lang = config.lang || null;
this.logger = Logger.getInstance();
this.bus = AgentEventBus.getInstance();
this.#projectRoot = config.projectRoot || process.cwd();
this.#additionalTools = config.additionalTools || [];
this.#toolPipeline = createToolPipeline();
this.#promptBuilder = new SystemPromptBuilder({
persona: this.persona,
fileCache: this.#fileCache,
lang: this.lang,
memoryConfig: this.memoryConfig,
});
this.state = new AgentState({
initialData: { runtimeId: this.id, preset: this.presetName },
});
this.bus.publish(AgentEvents.AGENT_CREATED, {
agentId: this.id,
preset: this.presetName,
capabilities: this.capabilities.map((c) => c.name),
strategy: this.strategy?.name,
}, { source: this.id });
}
// ─── 公共 API ─────────────────────────────────
/**
* 执行 Agent — 入口
*
* @param message 统一消息
* @param [opts] 策略特定选项 (如 FanOut 的 items)
*/
async execute(message, opts = {}) {
this.startTime = Date.now();
this.iterationCount = 0;
this.toolCallHistory = [];
this.tokenUsage = { input: 0, output: 0 };
// ── Policy: 执行前校验 ──
const beforeCheck = this.policies.validateBefore({ message, capabilities: this.capabilities });
if (!beforeCheck.ok) {
this.logger.warn(`[AgentRuntime] Policy rejected: ${beforeCheck.reason}`);
return {
reply: `⚠️ ${beforeCheck.reason}`,
toolCalls: [],
tokenUsage: { input: 0, output: 0 },
iterations: 0,
durationMs: 0,
state: this.state.toJSON(),
};
}
// ── 超时保护 ──
const budget = this.policies.getBudget();
const timeoutMs = budget?.timeoutMs || 300_000;
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(`Agent timeout after ${timeoutMs}ms`)), timeoutMs);
});
try {
// ── 委托给 Strategy ──
const resultPromise = this.strategy.execute(this, message, opts);
const result = (await Promise.race([resultPromise, timeoutPromise]));
clearTimeout(timeoutId);
// ── Policy: 执行后校验 ──
const afterCheck = this.policies.validateAfter(result);
if (!afterCheck.ok) {
this.logger.warn(`[AgentRuntime] Quality check: ${afterCheck.reason}`);
result.qualityWarning = afterCheck.reason;
}
// 状态完成
this.#safeTransition('finish', { reply: result.reply?.slice(0, 100) });
// 回复给原始渠道
if (message.replyFn && result.reply) {
await message.reply(result.reply);
}
result.state = this.state.toJSON();
result.durationMs = Date.now() - this.startTime;
this.bus.publish(AgentEvents.AGENT_COMPLETED, {
agentId: this.id,
preset: this.presetName,
iterations: result.iterations,
durationMs: result.durationMs,
}, { source: this.id });
return result;
}
catch (err) {
clearTimeout(timeoutId);
this.state.send('error', { error: err.message });
this.bus.publish(AgentEvents.AGENT_FAILED, {
agentId: this.id,
error: err.message,
}, { source: this.id });
throw err;
}
}
// ─── ReAct Loop — 供 Strategy 调用 ──────────
/**
* 核心 ReAct 循环。Strategy 调用此方法执行实际的 LLM + Tool 交互。
*
* 引擎级能力通过可选参数注入:
* - contextWindow → 三级递进压缩 + 动态工具结果限额
* - tracker → ExplorationTracker 阶段管理 + Nudge + Graceful exit
* - trace → ActiveContext 推理链记录
* - memoryCoordinator → 缓存/动态提示/观察记录
* - sharedState → 提交去重 { submittedTitles, submittedPatterns }
* - source → 'user' | 'system' (影响错误恢复 + 强制摘要行为)
*
* 向后兼容: 以上参数均为可选。不提供时退化为原始裸循环。
*
* @param prompt 用户/系统提示
* @param [opts.history] 对话历史
* @param [opts.context] 额外上下文
* @param [opts.capabilityOverride] 临时覆盖 capability (Pipeline 阶段用)
* @param [opts.budgetOverride] 临时覆盖 budget
* @param [opts.systemPromptOverride] 完全覆盖系统提示词 (Bootstrap 阶段专用)
* @param [opts.onToolCall] 本轮独立的工具调用钩子,优先于 runtime 级
* @param [opts.contextWindow] 上下文窗口管理器
* @param [opts.tracker] ExplorationTracker 实例
* @param [opts.trace] ActiveContext 实例
* @param [opts.memoryCoordinator] MemoryCoordinator 实例
* @param [opts.sharedState] 共享状态 { submittedTitles, submittedPatterns }
* @param [opts.source] 'user' | 'system'
* @param [opts.toolChoiceOverride] 首轮 toolChoice 覆盖 ('required'/'auto'/'none')
* 首轮强制 LLM 生成 tool call(LLM 自行决定调哪个工具、传什么参数)。
* 这不是替 LLM 做决定,而是告诉 LLM "你必须调用某个工具"。
* 仅在第一轮生效,后续轮次恢复正常 toolChoice 逻辑。
*/
async reactLoop(prompt, opts = {}) {
const ctx = this.#initLoop(prompt, opts);
// ─── ReAct 主循环 (编排骨架) ─────
while (true) {
ctx.iteration++;
this.iterationCount++;
// ActiveContext: 开始新轮次 (必须在 #shouldExit 前, 保证 endRound 有配对)
ctx.trace?.startRound(ctx.iteration);
// 退出判定 (tracker + policy)
if (this.#shouldExit(ctx)) {
break;
}
// 迭代准备 (hooks + nudge + compact + toolChoice + prompt)
const { toolChoice, effectiveSystemPrompt, effectivePrompt } = this.#prepareIteration(ctx);
// LLM 调用 (含错误恢复 + 空响应重试)
const llmResult = await this.#callLLM(ctx, toolChoice, effectiveSystemPrompt, effectivePrompt);
if (!llmResult) {
break;
}
if (llmResult.type === LLMResultType.CONTINUE) {
continue;
}
// ActiveContext: 记录 AI 的推理文本 + 提取/更新计划
if (ctx.trace && llmResult.text) {
ctx.trace.setThought(llmResult.text);
ctx.trace.extractAndSetPlan?.(llmResult.text, ctx.iteration);
}
// 分支: 有 Tool Call
if ((llmResult.functionCalls?.length ?? 0) > 0) {
const exitAfterTools = await this.#processToolCalls(ctx, llmResult, effectiveSystemPrompt);
if (exitAfterTools) {
break;
}
continue;
}
// 分支: 纯文本回复
if (this.#processTextResponse(ctx, llmResult)) {
break;
}
}
return this.#finalize(ctx);
}
// ─── 提取方法: reactLoop 内部阶段 ────────────
/** 初始化循环上下文 — 封装 reactLoop 前 ~60 行初始化逻辑 */
#initLoop(prompt, opts) {
const { history = [], context = {}, capabilityOverride, budgetOverride, systemPromptOverride, onToolCall, contextWindow, tracker, trace, memoryCoordinator, sharedState, source, toolChoiceOverride, abortSignal, } = opts;
// 解析 capabilities
const caps = capabilityOverride
? this.#resolveCapabilities(capabilityOverride)
: this.capabilities;
// 构建基础系统提示词 (委托 SystemPromptBuilder)
let baseSystemPrompt = systemPromptOverride || this.#promptBuilder.build(caps, context);
// 收集工具 (caps 为空数组时 = 明确无工具)
const allowedTools = this.#collectTools(caps);
const noToolsExplicit = Array.isArray(capabilityOverride) && capabilityOverride.length === 0;
const toolSchemas = noToolsExplicit
? []
: this.toolRegistry.getToolSchemas(allowedTools.length > 0 ? allowedTools : null);
// 创建统一消息适配器 (消除 useCtxWin 双模式)
const messages = createMessageAdapter(contextWindow);
// 加载历史 + 用户 prompt
for (const h of history) {
if (h.role === 'assistant') {
messages.appendAssistantText(h.content);
}
else {
messages.appendUserMessage(h.content);
}
}
messages.appendUserMessage(prompt);
// 预算
const budget = budgetOverride ||
this.policies.getBudget() || {
maxIterations: 20,
maxTokens: 4096,
temperature: 0.7,
};
// 系统源: 注入轮次预算 (委托 SystemPromptBuilder)
baseSystemPrompt = SystemPromptBuilder.injectBudget(baseSystemPrompt, {
source,
tracker,
budget,
});
// 状态转移
this.#safeTransition('start', { prompt: prompt.slice(0, 100) });
this.#safeTransition('plan_ready');
this.bus.publish(AgentEvents.AGENT_STARTED, {
agentId: this.id,
prompt: prompt.slice(0, 100),
capabilities: caps.map((c) => c.name),
}, { source: this.id });
return new LoopContext({
messages,
tracker: tracker || null,
trace: trace || null,
memoryCoordinator: memoryCoordinator || null,
sharedState: sharedState || null,
source: source || 'user',
budget,
capabilities: caps,
baseSystemPrompt,
toolSchemas,
prompt,
onToolCall: onToolCall || null,
context: context || {},
contextWindow: contextWindow || null,
toolChoiceOverride: toolChoiceOverride || null,
abortSignal: abortSignal || null,
});
}
/**
* 退出判定 — 合并 tracker/policy 退出检查
* @returns true = 应退出循环
*/
#shouldExit(ctx) {
// 外部中止信号 — 立即退出
if (ctx.abortSignal?.aborted) {
this.logger.info('[AgentRuntime] ⛔ abortSignal fired — exiting loop');
return true;
}
// ExplorationTracker: tick + 退出检查
if (ctx.tracker) {
ctx.tracker.tick();
if (ctx.tracker.shouldExit()) {
this.logger.info(`[AgentRuntime] tracker exit: phase=${ctx.tracker.phase}, iter=${ctx.tracker.iteration}, submits=${ctx.tracker.totalSubmits}`);
return true;
}
}
// Capability 前置钩子
for (const cap of ctx.capabilities) {
cap.onBeforeStep({
iteration: ctx.iteration,
messages: ctx.messages.toMessages(),
prompt: ctx.prompt,
});
}
// ── Per-stage budget timeout (从 budgetOverride 注入) ──
// 与 BudgetPolicy 的全局 timeoutMs (600s) 不同,此处使用阶段级 budget.timeoutMs
// 保证 Analyst/Producer 各自有独立的超时边界,避免一个阶段消耗完所有时长
if (ctx.budget?.timeoutMs && Date.now() - ctx.loopStartTime > ctx.budget.timeoutMs) {
this.logger.info(`[AgentRuntime] ⏰ Stage budget timeout: ${ctx.budget.timeoutMs}ms exceeded (elapsed: ${Date.now() - ctx.loopStartTime}ms)`);
return true;
}
// Policy 实时校验
// 当 ExplorationTracker 存在时,由 tracker 自己管理 maxIterations + grace 轮次,
// 跳过 BudgetPolicy 的 iteration 限制,避免竞争(tracker 给了 grace 但 policy 立即杀掉循环)。
// tracker 内部有硬上限 (maxIterations + 2) 保证不会无限循环。
const skipPolicyIterCheck = !!ctx.tracker;
const duringCheck = this.policies.validateDuring({
iteration: skipPolicyIterCheck ? 0 : ctx.iteration, // tracker 模式下用 0 绕过 iteration 检查
toolCalls: this.toolCallHistory,
tokenUsage: this.tokenUsage,
startTime: ctx.loopStartTime,
});
if (!duringCheck.ok) {
this.logger.info(`[AgentRuntime] Policy stop: ${duringCheck.reason}`);
return true;
}
return false;
}
/**
* 迭代准备 — 合并 nudge/压缩/提示词增强/toolChoice
* @returns }
*/
#prepareIteration(ctx) {
const { tracker, trace, capabilities: _capabilities, messages, prompt } = ctx;
const maxIterations = ctx.maxIterations;
this.#emitProgress('thinking', { iteration: ctx.iteration, maxIterations });
// Nudge 注入 (ExplorationTracker)
if (tracker) {
const nudge = tracker.getNudge(trace);
if (nudge) {
messages.appendUserNudge(nudge.text);
this.logger.info(`[AgentRuntime] 💬 injected ${nudge.type} nudge at iter ${ctx.iteration}`);
const _dim = ctx.sharedState?._dimensionMeta?.id || '';
if (process.env.ASD_MCP_MODE !== '1') {
process.stderr.write(`\n\x1b[36m━━━ Nudge [${nudge.type}] iter=${ctx.iteration}${_dim ? ` dim=${_dim}` : ''} ━━━\x1b[0m\n`);
process.stderr.write(`\x1b[33m${nudge.text}\x1b[0m\n\n`);
}
}
}
// 压缩检查
const compactResult = messages.compactIfNeeded();
if (compactResult.level > 0) {
this.logger.info(`[AgentRuntime] context compacted: L${compactResult.level}, removed ${compactResult.removed} items`);
}
// 动态 toolChoice
const forceSummaryAt = Math.max(2, Math.ceil(maxIterations * 0.8));
const forceSummary = !tracker && ctx.iteration >= forceSummaryAt;
let toolChoice;
if (ctx.toolChoiceOverride && ctx.iteration === 1) {
// 首轮 toolChoice 覆盖: 强制 LLM 生成 tool call (LLM 自行决定调哪个、传什么)
toolChoice = ctx.toolChoiceOverride;
}
else if (tracker) {
toolChoice = tracker.getToolChoice();
}
else {
toolChoice = ctx.toolSchemas.length > 0 ? (forceSummary ? 'none' : 'auto') : 'none';
}
// 系统提示词增强 (阶段上下文 + 动态记忆)
let effectiveSystemPrompt = ctx.baseSystemPrompt;
if (tracker) {
effectiveSystemPrompt += tracker.getPhaseContext();
}
else if (ctx.isSystem && !tracker) {
const remaining = maxIterations - ctx.iteration;
effectiveSystemPrompt += `\n\n## 当前进度\n第 ${ctx.iteration}/${maxIterations} 轮 | 剩余 ${remaining} 轮`;
}
if (ctx.isSystem && ctx.memoryCoordinator) {
const wmContext = ctx.memoryCoordinator.buildDynamicMemoryPrompt?.({
mode: (ctx.source || 'analyst'),
scopeId: ctx.context?.dimensionScopeId || undefined,
});
if (wmContext) {
effectiveSystemPrompt += `\n\n${wmContext}`;
}
}
// 非 tracker 模式的强制摘要提示注入
const effectivePrompt = forceSummary
? `${prompt}\n\n[系统提示] 已进入最后阶段,请停止调用工具,基于已有信息输出总结。`
: prompt;
return { toolChoice, effectiveSystemPrompt, effectivePrompt };
}
/**
* LLM 调用 — 含错误恢复 + 空响应重试
*
* @returns llmResult 或 null (表示应退出)
*/
async #callLLM(ctx, toolChoice, effectiveSystemPrompt, effectivePrompt) {
this.bus.publish(AgentEvents.LLM_CALL_START, {
agentId: this.id,
iteration: ctx.iteration,
}, { source: this.id });
let llmResult;
try {
// toolChoice='none' 时不发送 toolSchemas —— 部分 LLM (Gemini) 在看到
// 工具定义但被禁止调用时会返回空内容,导致 SUMMARIZE 阶段失败
const effectiveToolSchemas = toolChoice === 'none'
? undefined
: ctx.toolSchemas.length > 0
? ctx.toolSchemas
: undefined;
llmResult = (await this.aiProvider.chatWithTools(effectivePrompt, {
messages: ctx.messages.toMessages(),
toolSchemas: effectiveToolSchemas,
toolChoice: effectiveToolSchemas ? toolChoice : undefined,
systemPrompt: effectiveSystemPrompt,
temperature: ctx.budget.temperature ?? (ctx.isSystem ? 0.3 : 0.7),
maxTokens: ctx.budget.maxTokens ?? (ctx.isSystem ? 8192 : 4096),
abortSignal: ctx.abortSignal ?? undefined,
}));
ctx.consecutiveAiErrors = 0;
}
catch (aiErr) {
return this.#handleAiError(ctx, aiErr);
}
// 累计 Token (runtime 级 + loop 级)
if (llmResult.usage) {
this.tokenUsage.input += llmResult.usage.inputTokens || 0;
this.tokenUsage.output += llmResult.usage.outputTokens || 0;
ctx.addTokenUsage(llmResult.usage);
}
this.bus.publish(AgentEvents.LLM_CALL_END, {
agentId: this.id,
hasToolCalls: !!llmResult.functionCalls?.length,
hasText: !!llmResult.text,
usage: llmResult.usage,
}, { source: this.id });
// 空响应重试
if (!llmResult.text && !llmResult.functionCalls?.length) {
// B4 fix: SUMMARIZE 阶段也允许重试 — force_exit nudge 刚注入时 LLM 可能
// 需要额外一轮才能生成有效输出。与 ExplorationTracker 的 2 轮 grace 对齐,
// 避免 grace 机制被架空。重试次数由 tracker.phaseRounds 控制而非独立计数。
const isTerminal = ctx.tracker && ctx.tracker.phase === 'SUMMARIZE';
if (isTerminal && ctx.tracker) {
const phaseRounds = ctx.tracker.metrics?.phaseRounds ?? 0;
if (phaseRounds < 2) {
ctx.consecutiveEmptyResponses++;
this.logger.warn(`[AgentRuntime] ⚠ empty response in SUMMARIZE — retrying (grace ${phaseRounds + 1}/2)`);
// 不 rollbackTick: 让 tracker 计入 phaseRounds 以便到达 grace 上限退出
await new Promise((r) => setTimeout(r, 1500));
return continueResult();
}
this.logger.warn('[AgentRuntime] ⚠ empty response in SUMMARIZE (grace exhausted) — proceeding to forced summary');
return null;
}
if (ctx.isSystem && ctx.consecutiveEmptyResponses < 2) {
ctx.consecutiveEmptyResponses++;
this.logger.warn(`[AgentRuntime] ⚠ empty response — retrying (${ctx.consecutiveEmptyResponses}/2)`);
ctx.tracker?.rollbackTick?.();
await new Promise((r) => setTimeout(r, 1500));
// 返回 CONTINUE 信号 — 调用方需重走循环
return continueResult();
}
return null; // 退出
}
if (llmResult.text || llmResult.functionCalls?.length) {
ctx.consecutiveEmptyResponses = 0;
}
// Graceful exit 保护
if (ctx.tracker?.isGracefulExit &&
llmResult.functionCalls?.length &&
llmResult.functionCalls.length > 0) {
this.logger.warn(`[AgentRuntime] ⚠ AI returned ${llmResult.functionCalls.length} tool calls despite toolChoice=none (graceful exit) — ignoring`);
if (llmResult.text) {
ctx.lastReply = cleanFinalAnswer(llmResult.text);
return null; // 退出
}
return continueResult();
}
return llmResult;
}
/**
* AI 错误处理 — 熔断器感知 + 2-strike 策略
* @returns continueResult() 或 null (退出)
*/
async #handleAiError(ctx, aiErr) {
// AbortError — 外部中止信号已触发,不计入错误计数,立即退出
if (ctx.abortSignal?.aborted) {
this.logger.info('[AgentRuntime] ⛔ abortSignal fired during LLM call — exiting');
return null;
}
ctx.consecutiveAiErrors++;
this.logger.warn(`[AgentRuntime] AI call failed (attempt ${ctx.consecutiveAiErrors}): ${aiErr.message}`);
ctx.tracker?.rollbackTick?.();
// 熔断器感知
if (aiErr.code === 'CIRCUIT_OPEN') {
this.logger.warn('[AgentRuntime] 🛑 circuit breaker OPEN — breaking to summary');
if (!ctx.isSystem) {
ctx.lastReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
}
return null;
}
// 2-strike 策略
if (ctx.consecutiveAiErrors >= 2) {
this.logger.warn('[AgentRuntime] 🛑 2 consecutive AI errors — breaking to summary');
ctx.messages.resetToPromptOnly();
if (!ctx.isSystem) {
ctx.lastReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
}
return null;
}
await new Promise((r) => setTimeout(r, 2000));
return continueResult();
}
/**
* 工具调用处理 — 执行 + 记录 + 去重 + 阶段转换
*
* @param effectiveSystemPrompt 用于 budget 耗尽时的摘要调用
* @returns true = 应退出循环
*/
async #processToolCalls(ctx, llmResult, effectiveSystemPrompt) {
const { tracker, trace, messages } = ctx;
// 工具调用数量限制
let activeCalls = llmResult.functionCalls;
if (activeCalls.length > MAX_TOOL_CALLS_PER_ITER) {
this.logger.warn(`[AgentRuntime] ⚠ ${activeCalls.length} tool calls, capping to ${MAX_TOOL_CALLS_PER_ITER}`);
tracker?.recordTruncatedCalls?.(activeCalls.length - MAX_TOOL_CALLS_PER_ITER);
activeCalls = activeCalls.slice(0, MAX_TOOL_CALLS_PER_ITER);
}
// 追加 assistant 消息
messages.appendAssistantWithToolCalls(llmResult.text || null, activeCalls);
let roundSubmitCount = 0;
let roundHasNewInfo = false;
const roundToolNames = [];
// 执行每个工具
for (const fc of activeCalls) {
this.#emitProgress('tool_call', { tool: fc.name, args: fc.args });
this.bus.publish(AgentEvents.TOOL_CALL_START, {
agentId: this.id,
tool: fc.name,
}, { source: this.id });
// 通过 Pipeline 执行 (safety → cache → execute → observe → track → trace → dedup)
const { result: toolResult, metadata } = await this.#toolPipeline.execute(fc, {
runtime: this,
loopCtx: ctx,
iteration: ctx.iteration,
});
const durationMs = metadata.durationMs;
const toolEntry = {
tool: fc.name,
args: fc.args,
result: toolResult,
durationMs,
};
ctx.toolCalls.push(toolEntry);
this.toolCallHistory.push(toolEntry);
if (metadata.isNew) {
roundHasNewInfo = true;
}
roundToolNames.push(fc.name);
// onToolCall 通知
const effectiveHook = ctx.onToolCall || this.onToolCall;
if (effectiveHook) {
try {
effectiveHook(fc.name, fc.args, toolResult, ctx.iteration);
}
catch {
/* 观察者错误不中断 */
}
}
const toolResultObj = toolResult;
this.bus.publish(AgentEvents.TOOL_CALL_END, {
agentId: this.id,
tool: fc.name,
durationMs,
success: !toolResultObj?.error,
}, { source: this.id });
// 工具结果格式化 (统一通过 MessageAdapter)
let resultStr = messages.formatToolResult(fc.name, toolResult);
// 提交去重: pipeline 中间件已标记 metadata
if (metadata.dedupMessage) {
resultStr = metadata.dedupMessage;
}
else if (metadata.isSubmit) {
roundSubmitCount++;
}
// 进度回调 (tool_end 需要 resultStr.length)
this.#emitProgress('tool_end', {
tool: fc.name,
duration: durationMs,
status: toolResultObj?.error ? 'error' : 'ok',
error: toolResultObj?.error || undefined,
resultSize: resultStr.length,
});
// 追加 tool result
messages.appendToolResult(fc.id, fc.name, resultStr);
}
// ExplorationTracker: endRound → 检查阶段转换
if (tracker) {
tracker.updatePlanProgress?.(trace);
const transitionNudge = tracker.endRound({
hasNewInfo: roundHasNewInfo,
submitCount: roundSubmitCount,
toolNames: roundToolNames,
});
if (transitionNudge) {
messages.appendUserNudge(transitionNudge.text);
this.logger.info(`[AgentRuntime] 📝 injected ${transitionNudge.type} nudge (${tracker.phase})`);
const _dimT = ctx.sharedState?._dimensionMeta?.id || '';
if (process.env.ASD_MCP_MODE !== '1') {
process.stderr.write(`\n\x1b[35m━━━ Transition Nudge [${transitionNudge.type}] phase=${tracker.phase}${_dimT ? ` dim=${_dimT}` : ''} ━━━\x1b[0m\n`);
process.stderr.write(`\x1b[33m${transitionNudge.text}\x1b[0m\n\n`);
}
}
}
// ActiveContext: 关闭轮次
if (trace) {
trace.setRoundSummary?.({
newInfoCount: roundHasNewInfo ? 1 : 0,
totalCalls: activeCalls.length,
submits: roundSubmitCount,
cumulativeFiles: tracker?.getMetrics?.()?.uniqueFiles || 0,
cumulativePatterns: tracker?.getMetrics?.()?.uniquePatterns || 0,
});
trace.endRound?.();
}
// Capability 后置钩子
const stepToolEntries = ctx.toolCalls.slice(-activeCalls.length);
const stepResult = {
type: 'tool_calls',
toolCalls: stepToolEntries,
iteration: ctx.iteration,
};
for (const cap of ctx.capabilities) {
cap.onAfterStep(stepResult);
}
this.#safeTransition('step_done', stepResult);
// 检查预算 (非 tracker 模式)
if (!tracker && ctx.iteration >= ctx.maxIterations) {
const summary = (await this.aiProvider.chatWithTools(ctx.prompt, {
messages: messages.toMessages(),
systemPrompt: effectiveSystemPrompt,
toolChoice: 'none',
temperature: ctx.budget.temperature ?? 0.7,
maxTokens: ctx.budget.maxTokens ?? 4096,
}));
if (summary.usage) {
this.tokenUsage.input += summary.usage.inputTokens || 0;
this.tokenUsage.output += summary.usage.outputTokens || 0;
ctx.addTokenUsage(summary.usage);
}
ctx.lastReply = cleanFinalAnswer(summary.text || '');
return true; // 退出
}
this.#safeTransition('continue');
return false; // 继续循环
}
/**
* 文本响应处理 — tracker 阶段路由 + 非 tracker 直接终止
*
* @returns true = 应退出循环
*/
#processTextResponse(ctx, llmResult) {
const { tracker, trace, messages } = ctx;
if (tracker) {
// (setThought + extractAndSetPlan 已在主循环中统一处理)
const textResult = tracker.onTextResponse();
if (textResult.isFinalAnswer) {
ctx.lastReply = cleanFinalAnswer(llmResult.text || '');
this.logger.info(`[AgentRuntime] ✅ final answer — ${ctx.lastReply.length} chars, ${tracker.iteration} iters, ${ctx.toolCalls.length} tool calls`);
trace?.endRound?.();
return true;
}
if (textResult.needsDigestNudge) {
messages.appendAssistantText(llmResult.text || '');
messages.appendUserNudge(textResult.nudge);
this.logger.info('[AgentRuntime] 📝 injected SUMMARIZE nudge (text-triggered transition)');
const _dimD = ctx.sharedState?._dimensionMeta?.id || '';
if (process.env.ASD_MCP_MODE !== '1') {
process.stderr.write(`\n\x1b[34m━━━ Digest Nudge [SUMMARIZE]${_dimD ? ` dim=${_dimD}` : ''} ━━━\x1b[0m\n`);
process.stderr.write(`\x1b[33m${textResult.nudge}\x1b[0m\n\n`);
}
trace?.endRound?.();
return false; // continue
}
if (textResult.shouldContinue) {
messages.appendAssistantText(llmResult.text || '');
if (textResult.nudge) {
messages.appendUserNudge(textResult.nudge);
const _dimC = ctx.sharedState?._dimensionMeta?.id || '';
if (process.env.ASD_MCP_MODE !== '1') {
process.stderr.write(`\n\x1b[32m━━━ Continue Nudge${_dimC ? ` dim=${_dimC}` : ''} ━━━\x1b[0m\n`);
process.stderr.write(`\x1b[33m${textResult.nudge}\x1b[0m\n\n`);
}
}
trace?.endRound?.();
return false; // continue
}
}
// 非 tracker 模式: 文字回答即最终回答
ctx.lastReply = cleanFinalAnswer(llmResult.text || '');
trace?.endRound?.();
return true;
}
/** 循环退出后处理 — 强制摘要 + 构建返回值 */
async #finalize(ctx) {
// Scan 管线: 所有结果在 toolCalls 中 (collect_scan_recipe),不需要文本回复
// 直接跳过 forced summary,避免浪费一次 LLM 调用
if (!ctx.lastReply && ctx.tracker?.pipelineType === 'scan') {
const recipeCount = ctx.toolCalls.filter((tc) => (tc.tool || tc.name) === 'collect_scan_recipe').length;
ctx.lastReply = `[scan complete: ${recipeCount} recipes collected]`;
}
// 强制摘要 — 循环结束后无文本回复时,生成摘要
// 覆盖所有场景: 系统管线、tracker 管线、用户对话(有/无工具调用)
if (!ctx.lastReply) {
if (ctx.toolCalls.length > 0 || ctx.tracker || ctx.isSystem) {
const forcedResult = await produceForcedSummary({
aiProvider: this.aiProvider,
source: ctx.source,
toolCalls: ctx.toolCalls,
tracker: ctx.tracker ?? undefined,
contextWindow: ctx.contextWindow,
prompt: ctx.prompt,
tokenUsage: this.tokenUsage,
});
ctx.lastReply = forcedResult.reply;
if (forcedResult.tokenUsage) {
this.tokenUsage.input += forcedResult.tokenUsage.input || 0;
this.tokenUsage.output += forcedResult.tokenUsage.output || 0;
ctx.addTokenUsage({
inputTokens: forcedResult.tokenUsage.input || 0,
outputTokens: forcedResult.tokenUsage.output || 0,
});
}
}
else {
// 兜底: 既无工具调用也无文本回复
ctx.lastReply = '抱歉,AI 未能生成有效回复。请重试或换个问题。';
this.logger.warn(`[AgentRuntime] ⚠ finalize: no reply, no tool calls (iter=${ctx.iteration}) — fallback message`);
}
}
return ctx.buildResult();
}
// ─── 公共工具方法 ────────────────────────────
/** 中止执行 */
abort(reason = 'User aborted') {
this.#safeTransition('abort', { reason });
this.bus.publish(AgentEvents.AGENT_ABORTED, {
agentId: this.id,
reason,
}, { source: this.id });
}
/**
* 注入内存文件缓存(bootstrap 场景: allFiles 已在内存中,避免重复磁盘读取)
* @param files [{ relativePath, content, name }]
*/
setFileCache(files) {
this.#fileCache = files;
this.#promptBuilder.setFileCache(files);
}
/** 项目根目录 (供 ToolExecutionPipeline 等访问) */
get projectRoot() {
return this.#projectRoot;
}
/** 文件缓存 (供 ToolExecutionPipeline 等访问) */
get fileCache() {
return this.#fileCache;
}
/** 发送进度事件 (公开方法,供 ToolExecutionPipeline 中间件调用) */
emitProgress(type, data = {}) {
this.#emitProgress(type, data);
}
// ─── 私有方法 ────────────────────────────────
/**
* 安全状态转移 — 忽略不合法转移而不是抛异常。
*
* Pipeline/FanOut 场景下 reactLoop() 被多次调用,
* 第 2+ 次调用时状态已不在 IDLE,直接 send('start') 会抛错。
* 此方法在转移不合法时静默跳过,保证多阶段执行不中断。
*/
#safeTransition(event, payload = {}) {
try {
this.state.send(event, payload);
}
catch {
// 转移不合法 — 在多阶段场景中这是预期行为,静默跳过
}
}
/**
* 收集所有 Capability 的工具白名单
* 如果任一 Capability tools 为空数组, 返回空 (使用全部工具)
*/
#collectTools(caps) {
const toolSet = new Set();
let hasUnlimited = false;
for (const cap of caps) {
const tools = cap.tools;
if (!tools || tools.length === 0) {
hasUnlimited = true;
break;
}
for (const t of tools) {
toolSet.add(t);
}
}
// 合并调用方按需注入的额外工具 (不经 Capability,避免污染共享能力)
for (const t of this.#additionalTools) {
toolSet.add(t);
}
return hasUnlimited ? [] : [...toolSet];
}
/** 解析 capability 名称为实例 (Pipeline 阶段覆盖时调用) */
#resolveCapabilities(capNames) {
if (capNames == null) {
return this.capabilities;
}
if (capNames.length === 0) {
return []; // explicit empty = no tools
}
return capNames.map((name) => {
if (typeof name === 'object' && name instanceof Capability) {
return name;
}
// 先在已加载的 capabilities 中查找
const existing = this.capabilities.find((c) => c.name === name);
if (existing) {
return existing;
}
// 否则从注册表创建
return CapabilityRegistry.create(name);
});
}
/** 发送进度事件 */
#emitProgress(type, data = {}) {
const event = {
type,
agentId: this.id,
preset: this.presetName,
...data,
timestamp: Date.now(),
};
if (this.onProgress) {
this.onProgress(event);
}
this.bus.publish(AgentEvents.PROGRESS, event, { source: this.id });
}
}
export default AgentRuntime;