Agent 模块:扩展点与提醒

agent-runtime 的扩展 SPI 怎样把功能挂进 pi 的回合循环,agent-modules 十二个子包各管什么,以及系统提醒、失控守卫、会话报告与引用溯源的实现。

作者 David更新于 第 20 篇(共 33 篇)

pi 的 Agent 加上 agent-core 的 PiTurnRunner,只管“调模型、跑工具、再调模型”(见pi 的 Agent 循环与回合执行器)。提醒注入、上下文压缩、失控检测、工具输出预算、Plan 模式、目标这些横切功能,MiniMax Code 都做成挂在循环上的模块,分三层:

层位置规模职责
领域模块packages/agent-modules/*12 个子包,39383 行纯领域逻辑,依赖由宿主注入,不 import 运行时包
适配层packages/agent-extension5395 行把领域模块包装成 AgentExtension
扩展 SPIpackages/agent-runtime1417 行定义 ExtensionAPI,每个回合把所有扩展的贡献组装起来

适配层的自我定位写在入口文件的注释里(packages/agent-extension/src/index.ts:4):

This package owns only SPI glue. It does not construct module instances, choose a host profile, or provide a default extension list.

装哪些扩展由宿主 local-runtime-v2 决定。后面会看到,公开版本里有好几个适配器导出了却没有挂上。

怎么用

普通用户不直接写扩展,接触到的是这些开关和命令:

  • 失控守卫:~/.minimax-code/config.yaml(数据目录可用 MINIMAX_DATA_DIR 改,packages/tui/src/runtime/data-dir.ts:21)里的 runawayGuard.enabled,缺省为 true,远程下发的值优先(packages/config/src/runaway-guard-config.ts:17)。
  • 工具输出预算:toolResultCompaction.maxInlineKiB 等,见上下文管理与压缩。
  • /feedback:只在命令搜索里出现(packages/tui/src/tui/commands/catalog.ts:347),提交反馈时会附带一份从会话报告统计出来的诊断摘要。
  • beta.mcodeTools:开启后,模型 ID 以 MiniMax-M2.7 开头时,系统提醒里多一段 mcode-tools-master-reminder(packages/agent-modules/system-reminder/src/mcode-tools-master-reminder.ts:1)。
  • beta.desktopPlanModeAgentEntry:决定模型能不能主动调用 EnterPlanMode,见Plan 模式与目标。

contextManagement.disableSystemReminderModels 在配置里有定义,注释说列出的模型会跳过每轮提醒(packages/config/src/config.ts:873);但本地宿主构造 SystemReminderService 时没有传这个集合(packages/local-runtime/src/api/host-memory.ts:79),在 CLI 里不起作用。

扩展 SPI:五种贡献、九个事件

一个扩展就是 { id, description?, init(pi) }(packages/agent-runtime/src/types.ts:308)。init 里能用的 ExtensionAPI(types.ts:263)只有五类调用:registerTool(静态工具或每回合求值的工具工厂)、contributeSystemPrompt、contributeUserPromptPrefix、registerReminderProvider,以及用 on(event, handler) 订阅 HOOK_NAMES 里的九个事件(types.ts:162)。

注册表 Registry 有几条硬规矩:注册只能发生在 init 期间,事后再调直接抛错,防止扩展把 pi 存起来以后用(packages/agent-runtime/src/registry.ts:307);两个扩展贡献同名工具时组装失败(agent-runtime/src/registry.ts:461);每回合收集的提醒按 priority 降序排列(agent-runtime/src/registry.ts:413);扩展还可以按会话单独启停(agent-runtime/src/registry.ts:189)。

九个事件里有七个落在 step 级,由一张表映射成 agent-core 的 PiTurnHooks 字段(agent-runtime/src/registry.ts:126):

const HOOK_MAPPINGS = [
  { pi: 'onLlmCallPreparedHook', event: 'on_llm_call_prepared', adapter: 'context' },
  { pi: 'beforeLlmCallHook', event: 'before_llm_call', adapter: 'context' },
  { pi: 'afterLlmCallHook', event: 'after_llm_call', adapter: 'context' },
  { pi: 'beforeToolCallHook', event: 'before_tool_call', adapter: 'tool-context' },
  { pi: 'afterToolCallHook', event: 'after_tool_call', adapter: 'tool-context' },
  { pi: 'onHistoryChangedHook', event: 'on_history_changed', adapter: 'context' },
  { pi: 'onStepEndHook', event: 'on_step_end', adapter: 'context' },
] as const satisfies readonly {
  pi: keyof PiTurnHooks;
  event: HookName;
  adapter: 'context' | 'tool-context';
}[];

另外两个 turn_start、turn_end 不进 pi,由宿主在回合前后直接调用(packages/local-runtime-v2/src/service/turn-system/agent-host/local-agent-host.ts:240)。术语要注意:pi 的 “turn” 指一次模型回答加上它引出的工具执行,在 MiniMax 这边叫 step;MiniMax 的回合由若干 step 组成(packages/agent-core/src/pi-turn-runner/hooks.ts:241)。

事件在 pi 上的落点时机能做什么
before_llm_callAgent.transformContext每次模型请求前放行、跳过、直接给回答、只改本次请求、持久替换历史、追加隐藏消息、中止
on_llm_call_prepared包在 streamFn 外请求定型、发出之前只读观察
after_llm_callmessage_end 事件回答完整、工具未执行改写文本、要求重答、判失败
before_tool_call / after_tool_callAgent.beforeToolCall / afterToolCall每个工具调用前后拦截调用、改写结果
on_history_changed历史落盘时消息增量或整段替换后观察
on_step_endpi 的 turn_end 事件每个 step 结束观察,可往 agent 里塞转向消息

对应实现在 agent-core 的 pi-turn-runner 里:transformContext 在 packages/agent-core/src/pi-turn-runner/llm.ts:37 设置,只读观察者包在 packages/agent-core/src/pi-turn-runner/turn.ts:273,after_llm_call 在 packages/agent-core/src/pi-turn-runner/events.ts:87,工具钩子在 packages/agent-core/src/pi-turn-runner/tools.ts:79 与 pi-turn-runner/tools.ts:104。transformContext、beforeToolCall、afterToolCall 是 pi 上游 Agent 自带的回调;runner 另外用到的 onToolExecutionStart,以及 Agent 上的 shouldStopAfterTurn,来自 MiniMax 的补丁(third_party/pi-mono/MINIMAX_CHANGES.md:65、MINIMAX_CHANGES.md:191)。几条语义契约:

  • before_llm_call 的钩子按顺序执行,任何一个抛错都会中止这次模型调用(日志写明 fail-closed,pi-turn-runner/llm.ts:212);有钩子返回 appendMessage 后,后面的钩子不再执行(pi-turn-runner/llm.ts:290),所以宿主把只改请求的提醒排在追加型提醒之前(packages/local-runtime-v2/src/service/turn-system/agent-host/execution/executor.ts:752)。
  • after_llm_call 返回 retry 时,runner 先删掉这条回答里的工具调用,再用 followUp 追加一条用户消息让模型重答(pi-turn-runner/events.ts:123),被否决的回答不会产生副作用。
  • on_step_end 的钩子出错只记降级指标,循环照常继续(pi-turn-runner/events.ts:341)。

CLI 回合实际挂了什么

AgentRuntime 在宿主组装时创建一次(packages/local-runtime-v2/src/service/turn-system/agent-host/native-production-dependencies.ts:131),每个回合预检时调用 assembleTurn(local-agent-host.ts:200)。扩展清单来自三处,由 mergeNormalExtensions 合并(packages/local-runtime-v2/src/service/turn-system/production-composition.ts:737):

来源扩展
产品层(packages/local-runtime-v2/src/application/agent/runtime-agent-product.ts:209、packages/local-runtime-v2/src/compat/v1/agent-host.ts:256)浏览器工具、网站部署投影、目标预算守卫、local-builtin-tool-hooks(产品内置的工具前后处理)、代码审查
会话层(packages/local-runtime-v2/src/services.ts:979)引用溯源(受开关控制)、plan-mode、local-goal-verifier-readonly、local-goal-budget-summary、miniapp-control(有 Mini App 生命周期时才挂,services.ts:687)
宿主内置(production-composition.ts:510)session-report、runaway-guard、tool-output-budget

宿主内置的三个总是排在最后,而且不能被同 ID 的扩展替换,注释给的理由是它们要看到前面所有扩展对工具结果的改写:

Host-owned builtins establish terminal postconditions. They must observe every configured after_tool_call rewrite and cannot be replaced by a configured extension with the same id.

反过来,@mavis/agent-extension 还导出了 systemReminderExtension、contextManagerExtension、permissionExtension、skillsExtension、terminalResponseRecoveryExtension、runawayGuardShadowExtension,公开仓库里却没有生产代码挂载它们;createNativeAgentHostProductionDependencies 的注释也写明,工厂本身不会自动启用 agent-extension 的内置适配器,只装调用方显式传入的那些(native-production-dependencies.ts:104)。CLI 走的是别的路:系统提醒由宿主直接拼进用户消息(见下文),压缩由 v2 自己的压缩器负责,权限走 LocalTurnPermissionGate(见权限模式与规则)。

十二个子包

子包行数做什么详见
system-reminder3407<system-reminder> 块的提供者链与组装本篇
conversation-contract751会话、队列、回合与压缩的运行时无关契约本篇
session-report620模型调用环境记录与会话诊断包收集本篇
runaway-guard1438回合内的打转检测与一次性提醒本篇
context-manager1511压缩阈值公式、token 估算、共享的 ContextManager上下文管理与压缩
goal3090线程目标的状态、工具、续跑提示与验证Plan 模式与目标
background-task916后台任务的领域模型与生命周期原语子 Agent 与后台任务
cron2921定时任务的调度与执行定时任务
skills1292基于文件系统的技能注册表技能
plugin-hooks5194插件 Hook 的解析、命令执行与会话生命周期协调插件系统
mcp1644MCP 运行时底座MCP
permission16599工具调用的权限决策引擎权限模式与规则

行数按全书统一口径统计(排除测试)。permission 一个子包就占了四成多。

系统提醒(system-reminder)

SystemReminderRegistry(packages/agent-modules/system-reminder/src/registry.ts:25)按注册顺序保存提供者,用 appendCritical 登记的“关键”提供者在某个模型被整体关掉提醒时仍然运行。SystemReminderService.buildReminder(packages/agent-modules/system-reminder/src/service.ts:88)每调用一次把会话轮数加一,让宿主实现的 DataCollector 收集数据,依次运行提供者,把非空输出拼成一个块(service.ts:210):

      const fullText =
        blocks.length === 0
          ? null
          : `<system-reminder>\n${blocks.join('\n\n')}\n</system-reminder>`;

createDefaultRegistry(packages/agent-modules/system-reminder/src/providers.ts:823)装了 26 个提供者,其中 4 个是关键提供者(待投递的会话提醒、Todo 收尾提醒、媒体产出提醒、异步审计)。按内容大致分几类:<agent-context>(agent 名、会话 ID 等身份信息,第二轮起换精简版,providers.ts:193)、团队协作、十来个记忆相关的块、Todo 收尾提醒(有未完成的 Todo 时,距上次提醒满 5 轮再提醒,providers.ts:425)、冷启动检查、媒体与密钥环境提示、IM 渠道的入站元数据。每个提供者自己判断数据齐不齐,不满足就返回空。

CLI 里,这套服务由第一代运行时的宿主设施 LocalSystemReminderService 持有(packages/local-runtime/src/api/host-memory.ts:65),v2 在回合输入准备阶段通过 reminders.buildSystem 取回结果(packages/local-runtime-v2/src/service/turn-system/agent-host/assembly/local-turn-input-preparation.ts:199),校验它恰好是一个完整的 <system-reminder> 块或空串(local-turn-input-preparation.ts:868),再和附件提醒(local-turn-input-preparation.ts:647)、斜杠技能提醒(local-turn-input-preparation.ts:831)排成 reminderBlocks(local-turn-input-preparation.ts:251)。执行器把它们连同扩展贡献的提醒一起拼到用户消息正文前面(executor.ts:502):

    const userPromptPrefix = joinUserPrompt(
      input.assembly.userPromptPrefix,
      renderAgentRuntimeReminders(input.assembly.reminders),
      joinPrompt(
        preparedExecution.reminderBlocks.join('\n\n'),
        input.pluginHookContext
          ? `<plugin-hook-context>\n${input.pluginHookContext}\n</plugin-hook-context>`
          : '',
      ),
      '',
    );

顺序是:扩展的用户提示前缀、扩展的提醒(如 Plan 模式提醒,按优先级排好)、宿主的提醒块与插件 Hook 上下文,最后才是用户输入;用户真正输入的那一段另用 genuineUserQueryText 标出。扩展提醒同样必须是完整的单个块,否则直接抛错(packages/local-runtime-v2/src/service/turn-system/agent-host/execution/prompt.ts:6)。

回合进行中还有几种只在请求前插入的提醒,都挂在 before_llm_call 上:带截止时间的回合会在每次请求末尾附上剩余秒数,只改本次请求、不进历史(packages/local-runtime-v2/src/service/turn-system/agent-host/runner/execution-budget-reminder.ts:10);Todo 节奏提醒在距上次 todowrite 和上次提醒都满 15 次 assistant 迭代后,以隐藏消息追加进历史(packages/local-runtime-v2/src/service/turn-system/execution/reminder/todo-cadence-reminder.ts:14、packages/local-runtime-v2/src/service/turn-system/compaction/compat.ts:10);后台任务提醒同理。追加前都要过 fitsReminderInFinalRequest,加上提醒会超出 provider 输入上限就放弃(packages/local-runtime-v2/src/service/turn-system/execution/reminder/reminder-admission.ts:8)。

会话契约(conversation-contract)

RuntimeConversation(packages/agent-modules/conversation-contract/src/index.ts:516)把运行时对外的会话能力收成四组:query(读会话与消息)、lifecycle(建、改、删会话)、ingress(提交、恢复用户输入、转向、队列的查询修改取消与重排、中止、派发,conversation-contract/src/index.ts:451)、maintenance(目前只有 compact,conversation-contract/src/index.ts:510)。local-runtime 和 local-runtime-v2 各有三十多个文件 import 这个包,它是两代运行时共用的会话接口。ConversationSource(conversation-contract/src/index.ts:1)列出回合的来源,如 api、cron、thread-goal、channel:feishu;ConversationSession.runtime 至今保留 opencode 这个取值(conversation-contract/src/index.ts:91),是旧底座留下的痕迹。DeferredRuntimeConversation(conversation-contract/src/index.ts:580)先把接口交出去,调用会等到真正的实现绑定之后再转发。

会话报告(session-report)

采集这一半是宿主内置扩展 sessionReportExtension(packages/agent-extension/src/session-report.ts:13)。它订阅 on_llm_call_prepared(session-report.ts:64),把一次模型调用里消息以外的部分——系统提示词、工具 schema、模型与 provider、思考级别、输出上限、缓存策略——写进会话目录下的 llm-call.json(packages/agent-modules/session-report/src/llm-call-report-store.ts:12)。内容不变就跳过写入(按 SHA-256 指纹比较),单个信封上限 4 MiB(llm-call-report-store.ts:14);压缩完成时把它冻结到对应快照旁边(llm-call-report-store.ts:57),回退(rewind)后删掉失效的那份(llm-call-report-store.ts:77)。写入失败只上报,不打断模型调用。历史消息本来就落盘,缺的正是这部分“调用环境”,两者合起来才能复原一次请求。

打包这一半是 SessionReportService.collect(packages/agent-modules/session-report/src/session-report-service.ts:47):遍历会话及其子会话的目录收集文件,跳过 llm-context-inspector 等目录(session-report-service.ts:34),task 会话额外附上 Agent 定义;读不到的文件记进 session-report-collection.json,不让整体失败。TUI 的 /feedback 经 uploadTuiFeedbackDiagnostics(packages/tui/src/runtime/feedback/diagnostic-upload.ts:70)读取这份清单和最近两天的日志,逐个文件压成计数、状态、错误码之类的摘要再打包上传(diagnostic-upload.ts:120),归档里的文件名一律换成不透明的编号(diagnostic-upload.ts:100);提交前的预览会列出这些范围,并写明原文与文件名不上传(packages/tui/src/runtime/feedback/service.ts:139)。

失控守卫(runaway-guard)

失控守卫只管“同一回合里模型原地打转”,从不拦工具,也不中止回合。宿主这样创建它(packages/local-runtime-v2/src/service/turn-system/runaway-guard/extension.ts:13):

export function createProductionRunawayGuardExtension(options: RunawayGuardHostOptions) {
  const emit = createRunawayGuardObserver(options);
  const local = readBestEffort(options.readLocalConfig);
  return runawayGuardExtension({
    toolPolicies: createProductionRunawayGuardToolPolicies(),
    readVerifiedProgress: options.readVerifiedProgress,
    isEnabled: () =>
      resolveRunawayGuardConfig(local, readBestEffort(options.readRemoteConfig)).enabled,
    shouldRemind: (ctx) => ctx.turnIntent?.kind !== 'goal-verifier',
    onSignal: (value) => emit('runaway_guard_signal', value),
    onReminder: (value) => emit('runaway_guard_reminder_injected', value),
    onTurnSummary: (value) => emit('runaway_guard_turn_summary', value),
  });
}

本地配置只在创建时读一次,远程值每次判定都重新读;目标验证子 Agent 的回合只观测不提醒。判定发生在每个 step 结束时:

  1. 投影:本 step 的每个工具调用被投影成几种指纹——动作(工具名加参数,工具策略可以只挑稳定字段)、结果(工具名、内容、details、是否出错)、错误族(优先取 details 里的错误码,否则按正则归到 timeout、rate_limit 等八类,再不行取前 1024 个字符,packages/agent-modules/runaway-guard/src/step-view.ts:388)、进度(由工具策略或宿主提供的循环键与进度键)。指纹是用每回合随机 32 字节密钥算的 HMAC(packages/agent-modules/runaway-guard/src/state.ts:101),原始参数不会外泄。
  2. 计数:同一个键在相邻 step 里连续出现就累加,断开即清零(packages/agent-modules/runaway-guard/src/signals.ts:158)。六种信号见 RunawayGuardSignalKind(packages/agent-modules/runaway-guard/src/contracts.ts:3):重复动作、重复结果、同一错误族、ABAB 交替(看最近四个 step 的动作批次,signals.ts:199)、轮询无变化、进度无变化。
  3. 阈值:计数到 2 发一次观测事件(signals.ts:174);到提醒阈值(默认 3,也不许设得更小,packages/agent-extension/src/runaway-guard.ts:69、packages/agent-modules/runaway-guard/src/guard.ts:31)时,其中四种信号可以触发提醒,按“进度无变化、同一错误族、重复动作、轮询”的优先级取一个(packages/agent-modules/runaway-guard/src/reminder.ts:10)。
  4. 提醒:每回合最多一次(reminder.ts:26),用 event.agent.steer() 作为一条用户消息放进 pi 的转向队列(runaway-guard.ts:130),在下一次模型请求前送达。

重复动作的提醒原文是:

Do not repeat it unchanged. Inspect the results already available, then either change strategy with a concrete expected state change, or report the blocker.

每条提醒末尾还补一句,声明这只是本回合的临时提醒,不要写进记忆或技能(reminder.ts:59)。工具策略(packages/local-runtime-v2/src/service/turn-system/runaway-guard/tool-policy.ts:7)排除了常见误报:bash、sh、shell、zsh 里单独的 rg、grep、git grep 以退出码 1 结束算“没搜到”而不是出错,grep 工具的 “No matches found” 同理;task_query、task_output 按轮询处理,用任务状态和输出游标判断有没有进展。

空回答恢复(terminal-response-recovery)

terminalResponseRecoveryExtension(packages/agent-extension/src/terminal-response-recovery.ts:36)盯的是另一种卡住:工具全部成功之后,模型交回一条既没有工具调用、也没有可见文字的回答。它在 on_step_end 记下上一批工具是否全部成功,after_llm_call 遇到这种空回答就返回 retry,附上一句提示(terminal-response-recovery.ts:28):

The tools completed, but your response was empty. Provide a concise final response summarizing the result for the user.

再空一次就返回 fail,原因记为 terminal_empty(terminal-response-recovery.ts:69)。前面说过,公开版本的 CLI 没有挂这个扩展。

引用溯源(source-reference)

agent-extension 里最大的文件是 packages/agent-extension/src/source-reference.ts,2434 行,加上 files、web、utils、marker 四个辅助文件共 2944 行。扩展做两件事(source-reference.ts:145):

  • 工具调用之后(source-reference.ts:155):MCP、App、文件三个适配器把一次成功的工具调用归一成 ToolSourceReference,写进工具结果的 details.source_references;App、MCP 与网页抓取的结果末尾再追加一个只给模型看的标记块。标记块由宿主创建并登记在一个 WeakSet 里(packages/agent-extension/src/source-reference-marker.ts:5),工具输出伪造不了;工具输出预算归档大结果时会保留这些标记(packages/agent-extension/src/tool-output-budget.ts:79)。
  • 模型回答之后(source-reference.ts:246):对不含工具调用的最终回答规范化引用写法;如果回答确实用了某个来源的内容却没标引用,就要求重答一次,原因 missing_source_citation(source-reference.ts:337),否则只改写文本。

宿主一侧已经接好(packages/local-runtime-v2/src/application/agent/source-reference-extension.ts:12 负责把附件与工作区文件解析成来源),能不能挂上取决于 isLocalSourceProvenanceEnabled;而它最终调用的 resolveSourceProvenanceEnabled 目前对所有环境都返回 false(packages/shared/src/source-provenance.ts:10),注释说是在排查疑似由溯源流程引发的主线程卡顿(source-provenance.ts:4)。

下一篇:上下文管理与压缩——自动压缩何时触发、检查点摘要怎样生成,以及压缩后如何重算上下文用量。

本页目录