@agent-forge/plugin-steering-guard
Agent Forge 中途引导落账提醒插件(docs/design/中途引导与任务状态联动设计.md §4.1 路线 A、§7 S5,第二层强化):检测到 run 进行中到达的用户引导时,向下一次模型请求的视图尾部注入一条 display:false 的提醒,提示模型按六类分类引导并按跨面落账顺序在本回合内落账。第一层常驻纪律由 goal 插件的 TASK_TOOLS_GUIDE 承载,本插件只做决策点提醒,不重复常驻文本。
bundled 分发,宿主通过 manifest 加载;禁用面为会话装配的 disabledPluginIds(agent-forge.plugin.steering-guard)。提醒只在请求视图内生存(不写 context.messages、不进 newMessages,不持久化),对用户不可见。
行为契约
- 探测:宿主在
_runInputHandlers内联 awaitbeforeInputloop 钩子,先于 prompt 路径的 run 激活、也先于 steer/followUp 入队返回。钩子时刻读SessionAPI.isIdle()是确定性的:idle ⇒ 该输入开新 run(清零 pending),busy ⇒ 运行中(含压缩中)到达的引导(pending 加一)。 - 注入:
transformContext链路在 pending > 0 且任务面在场时,向请求视图尾部追加一条提醒并消费一个计数(同一引导只提醒一次;run 内连发多条引导各提醒一次)。 - 生效边界:
SessionAPI.getActiveTools与任务面工具(goal_set/plan_set/todo_update/workflow_set/workflow_advance)无交集时不注入(计数保留,下一请求再判);getActiveTools不可用时视为在场(宁可多提醒);宿主无SessionAPI时插件保持惰性(一切输入按 run-start 处理,零提醒)。 - 压缩边界:不维护消息计数基线,不存在压缩致基线失真的问题;压缩期间
isIdle()为 false,压缩窗口内到达的引导照常注入(确定性测试钉住)。
探测结论(实施前源码验证)
input.received@1不流经CapabilityAPI.subscribe可见的总线:该事件经dispatchInputReceivedLifecycle只派发到 capability runtime 的 lifecycle 目录通道(dispatchLifecycleImpl只读lifecycleRegistrations),而subscribe/publish是独立的插件间总线(宿主从不向其发布宿主事件,plan.changed等均由插件自身api.publish)。- lifecycle 通道的派发是 fire-and-forget 异步(宿主
void dispatch...),相对 run 激活(runDefaultLoop置_isAgentRunActive)无顺序保证——"新 prompt 开新 run"路径在派发时刻读isIdle()是竞态,无法确定性满足"首请求/新回合首请求不提醒"。 - 故探测事件选
beforeInputloop 钩子(与transformContext同属 loop-hook 公共通道),状态机与决策矩阵全为纯函数(src/detector.ts)。
测试
npm test(确定性,零真实 AI/网络):
test/detector.test.ts:决策矩阵逐格(首请求/新回合/steering/去重/重放/连发/压缩窗口/任务面缺席/默认在场)。test/entry.test.ts:公共面接线(钩子注册、信号映射、注入与去重、门控保留计数、dispose)。test/manifests.test.ts:三份清单(plugin.json / agent-forge.json / package.json)一致性。test/registration.test.ts:manifest 通道真实装载(需先构建 dist:npm run build)。