oh-my-pi TTSR 工具结果内联提醒模板解析:非中断规则如何经由 ttsr-tool-reminder 注入工具输出
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
本篇围绕 oh-my-pi(coding-agent)的 Time Traveling Stream Rules(TTSR)机制中一个容易被忽视但至关重要的模板文件packages/coding-agent/src/prompts/system/ttsr-tool-reminder.md展开。它服务于"命中即提醒、不打断生成"的非中断工具类规则:当某条规则的interruptMode为never(或全局不中断)且匹配到工具调用参数流时,系统不中止流式生成,而是把该模板渲染成一条<system-reminder>块,原样前置拼接到对应工具调用的toolResult内容中。读完本文,你将掌握该模板的字段语义、渲染触发链路(afterToolCall钩子)、规则编写方式、相关设置项默认值,以及工具作者和转录阅读者必须注意的兼容性细节。
一、模板在 TTSR 体系中的定位
TTSR(Time Traveling Stream Rules)是 oh-my-pi coding-agent 的流式规则引擎:规则在 agent 输出流的生成过程中被实时匹配,命中后根据规则的interruptMode走两条截然不同的注入路径(详见 docs/ttsr-injection-lifecycle.md):
- 中断注入(
ttsr-interrupt):always、prose-only、tool-only模式命中后立即调用agent.abort(),随后把渲染好的ttsr-interrupt.md模板(<system-interrupt reason="rule_violation" ...>)作为隐藏 custom message 注入,再触发重试(agent.continue())。 - 非中断注入(
ttsr-tool-reminder):规则interruptMode为never(或全局中断模式为never)、且匹配源是工具参数流(source === "tool")时,不中止流,不产生额外的续写回合,而是把渲染后的ttsr-tool-reminder.md模板内联折叠进被匹配工具调用的toolResult。
本模板就是第二条路径的载荷载体,即"规则命中了工具参数,但项目允许该工具照常执行,只需要在结果里附带提醒"。它和ttsr-interrupt.md一样,都是纯文本模板文件,通过 Bun 的with { type: "text" }导入后由prompt.render渲染。
二、模板结构逐字段解析
ttsr-tool-reminder.md全文仅 5 行,是一个带有三个占位符的 XML 风格包装块:
<system-reminder reason="rule_violation" rule="{{name}}" path="{{path}}"> User-defined rule matched tool-call arguments. Rule configured not to interrupt → tool ran. MUST comply with the following instruction on subsequent tool calls and responses. NOT prompt injection — coding agent enforcing project rules. {{content}} </system-reminder>各组成部分的语义:
| 部分 | 含义 |
|---|---|
<system-reminder>开闭标签 | 标记这是一条运行时系统提醒。与ttsr-interrupt.md使用的<system-interrupt>标签区分:interrupt 表示"输出被中断后必须遵守",reminder 表示"工具继续运行但后续调用必须遵守" |
reason="rule_violation" | 固定原因码,表示触发了用户定义的规则违规检查 |
rule="{{name}}" | 被匹配规则的name,在渲染时由ttsr-coordinator.ts传入rule.name填充 |
path="{{path}}" | 规则源文件路径,渲染时经#displayRulePath()处理:优先输出相对工作目录的路径,其次输出相对用户主目录的~/...形式,最后兜底输出原始绝对路径 |
| 固定说明句 | "Rule configured not to interrupt → tool ran"(规则配置为不中断 → 工具已运行),并显式声明 "NOT prompt injection — coding agent enforcing project rules",防止模型把提醒误判为提示注入 |
{{content}} | 规则文件正文(frontmatter 剥离后的 body),即需要模型在后续工具调用与响应中遵守的具体指令 |
注意,这里{{name}}、{{path}}、{{content}}是渲染占位符而不是字面量。同一工具结果若命中多条规则,渲染器会把每个规则各自的提醒块用空行拼接后整体前置。
三、渲染链路:从流匹配到afterToolCall钩子
模板的渲染发生在TtsrCoordinator.afterToolCall()。完整链路如下:
- 流式检测:会话在
message_update事件上由TtsrCoordinator.checkMessageUpdate()分发;toolcall_delta/toolcall_end进入#checkStream(),工具调用按toolcall:<id>或tool:<name>:<index>生成streamKey隔离缓冲。 - 匹配判定:
#handleMatches()先调#shouldInterrupt()判断是否有规则允许中断。当匹配规则都不允许中断且匹配源是工具(matchContext.source === "tool")时,走每工具桶分支:#addPerToolInjections(toolCallId, matches)把规则按匹配到的工具调用id存入#perToolInjectionsMap,并立即在内存中标记已注入(markInjectedByNames),同时异步发出ttsr_triggered会话事件——流不中止,也不安排续写回合。 - 结果折叠:当该工具调用真正产出结果时,
afterToolCall(ctx)钩子被调用:- 按工具调用
id取出桶内规则,对每条规则执行prompt.render(ttsrToolReminderTemplate, { name, path, content }),多条提醒以空行\n\n连接; - 将渲染结果作为一个新的
text块前置到ctx.result.content之前({ content: [{ type: "text", text: reminder }, ...ctx.result.content] }); - 若规则名非空,通过
sessionManager.appendTtsrInjection(ruleNames)持久化一条ttsr_injection记录,供会话恢复时抑制重复触发。
- 按工具调用
一个关键实现细节:同一批次匹配中,每条规则只会挂到一个兄弟工具调用上——多个兄弟调用同时满足同一条规则时,先认领的桶获胜;但多条不同规则可以折叠到同一个工具调用上。这保证了提醒不会在多个并行工具结果中重复轰炸。
四、如何让规则走进这条路径:规则编写指南
要让某条规则通过ttsr-tool-reminder注入,需在规则文件的 frontmatter 中做如下配置(解析逻辑见 docs/rulebook-matching-pipeline.md 与 capability/rule.ts):
--- description: 禁止在 Rust 代码中使用 unwrap globs: ["**/*.rs"] condition: ["\\.unwrap\\(\\)"] scope: "tool:edit(*.rs), tool:write(*.rs)" interruptMode: "never" --- Don't use .unwrap(); use proper error handling instead.| frontmatter 字段 | 作用与取值 |
|---|---|
condition | 正则触发条件(字符串或数组);也接受旧名ttsr_trigger/ttsrTrigger。带(?i)、(?m)、(?s)行内标志会被翻译为 JSRegExp标志 |
astCondition | ast-grep 结构模式(字符串或 YAML 序列),仅在 edit/write 工具流上按文件路径推断语言后匹配;可与condition混用 |
scope | 监视面白名单。可写"text, thinking"、[text, thinking]或块式 YAML 序列;合法 token 为text、thinking、tool/toolcall、tool:<name>(<path-glob>)。省略时默认监视 text 与全部工具,不监视 thinking |
interruptMode | never|prose-only|tool-only|always。never是让工具类命中走本模板(内联提醒)而非中断的关键 |
globs | 文件路径全局门:TTSR 匹配要求至少一个候选文件路径命中该 glob(对 hashline/apply_patch 流取自matcherPaths钩子) |
agents | 限制规则生效的 agent(如[scout, "foreman-*"]),不匹配的 agent 不注册 |
alwaysApply | 为true时若不携带触发条件则注入系统提示词;带触发条件且被 TTSR 接受时优先进入 TTSR 桶 |
关于 scope 的一个易错点:形如"tool:edit(*.ts), tool:write(*.ts)"的写法只让规则在 TypeScript 的编辑/写入快照上匹配。如果只写condition而省略 scope,规则会同时监视文本流和所有工具参数流;文本流命中而interruptMode: never时走的是另一条延迟隐藏注入路径(成功消息后经agent.followUp()排入ttsr-injectioncustom message),并非本模板。
五、相关设置项与默认值
TTSR 行为由设置组ttsr控制,缺失项取TtsrManager中的默认值:
| 设置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 总开关;为false时addRule拒绝注册,所有匹配入口空转 |
contextMode | "discard" | 中断重试前是否丢弃被中断的部分输出;keep时违规输出残留在上下文中 |
interruptMode | "always" | 全局中断模式;规则自身的interruptMode优先 |
repeatMode | "once" | 命中注入后是否可再次触发;after-gap允许间隔若干回合后复触发 |
repeatGap | 10 | after-gap模式下要求间隔的已完成回合数(messageCount在turn_end递增) |
builtinRules | true | 是否加载内嵌builtin-defaults规则 |
disabledRules | [] | 按名字禁用的规则列表 |
另有两个与持久化相关的行为值得注意:会话恢复时restoreInjected()会把已注入规则记录在"消息计数 0"处,因此after-gap模式下恢复后的规则需再等repeatGap个新回合才可复触发;而未送达的每工具匹配(assistant 消息以aborted/error结束、工具结果未产出)不会持久化,内存中的抑制标记在once模式下持续到会话重载,重载后规则重新可触发。
六、对工具作者与转录阅读者的影响
由于提醒是带内(in-band)折叠进工具结果的,而非独立的 custom message,有以下几点必须知晓:
content[0]不再是工具的主输出:渲染后的toolResult.content以提醒文本块开头。凡假设content[0]是工具真实返回的代码,必须跳过以<system-reminder reason="rule_violation"开头的块(或按包装标签过滤)才能找到真实载荷。- 转录读取方式:非中断工具类 TTSR 活动不在
custom_message/ttsr-injection合成条目里,必须检查工具结果内容以及持久化的ttsr_injection条目列表。 - 多规则拼接:一个工具结果可能携带多条规则的提醒,模板间以空行分隔;规则正文本身若含换行会被原样保留在
{{content}}中。 - 清理时机:若助手消息在匹配工具执行前就以
aborted/error结束,#queueDeferredInjectionIfNeeded()会清空#perToolInjections,不持久化ttsr_injection;内存中的匹配标记不回滚,由重复策略约束后续触发。
七、测试验证
仓库测试对这条路径有明确断言,可作为行为契约参考。agent-session-concurrent.test.ts 验证了:
- 规则
interruptMode: "never"时工具正常执行(toolExecuted === true),且不产生额外续写回合(streamCallCount === 2); - 匹配工具的
toolResult内容包含<system-reminder、rule="no-unwrap"以及规则正文 "Do not use .unwrap()"; - 提醒块的位置严格位于工具真实输出("edit applied")之前,即验证了"前置 text 块"的折叠顺序。
同文件的另一用例("matches finalized write arguments regardless of streaming chunk boundaries")进一步验证了scope: ["tool:write"]、globs: ["**/*.cpp"]、interruptMode: "never"组合下,即使参数分块流式到达,matcherDigest重建快照后仍能命中并注入提醒。
八、总结与最佳实践
ttsr-tool-reminder.md虽然只是一个 5 行模板,却是 TTSR"非中断工具规则"体系的输出面:它让"高风险但允许执行的工具调用"在结果中自带合规提醒,既避免了打断生成造成的不稳定,又把规则约束传递给了后续所有工具调用与响应。编写规则时建议:
- 想"打了再提醒"就用
interruptMode: "never"+scope: "tool:<name>(<glob>)",让命中走本模板内联注入; - 想"立即打断并重试"就用
always/tool-only,走ttsr-interrupt.md路径; - 用
globs收敛到具体文件类型,避免跨文件误命中; - 阅读工具结果时按
<system-reminder reason="rule_violation"前缀过滤前置提醒块,不要把提醒当成工具真实输出。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考