@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -0,0 +1,496 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import {
4
+ type AfterToolCallContext,
5
+ type AfterToolCallResult,
6
+ type Agent,
7
+ type AgentEvent,
8
+ type AgentMessage,
9
+ createToolScopedAbortReason,
10
+ } from "@oh-my-pi/pi-agent-core";
11
+ import type { AssistantMessage, ToolCall } from "@oh-my-pi/pi-ai";
12
+ import { isRecord, prompt, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
13
+ import type { Rule } from "../capability/rule";
14
+ import type { Settings } from "../config/settings";
15
+ import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
16
+ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
17
+ import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
18
+ import type { AgentSessionEvent } from "./agent-session-events";
19
+ import type { SessionManager } from "./session-manager";
20
+
21
+ interface TtsrContinueOptions {
22
+ delayMs?: number;
23
+ generation?: number;
24
+ shouldContinue?: () => boolean;
25
+ onSkip?: () => void;
26
+ onError?: () => void;
27
+ }
28
+
29
+ /** Capabilities the TTSR coordinator borrows from its owning session. */
30
+ export interface TtsrCoordinatorHost {
31
+ agent: Agent;
32
+ sessionManager: SessionManager;
33
+ settings: Settings;
34
+ emitSessionEvent(event: AgentSessionEvent): Promise<void>;
35
+ schedulePostPromptTask(task: (signal: AbortSignal) => Promise<void>, options?: { delayMs?: number }): void;
36
+ scheduleAgentContinue(options: TtsrContinueOptions): void;
37
+ promptGeneration(): number;
38
+ }
39
+
40
+ /** Coordinates TTSR stream matching, interruption, injection, and resume gates. */
41
+ export class TtsrCoordinator {
42
+ readonly #host: TtsrCoordinatorHost;
43
+ readonly #manager: TtsrManager | undefined;
44
+ #pendingInjections: Rule[] = [];
45
+ #perToolInjections = new Map<string, Rule[]>();
46
+ #abortPending = false;
47
+ #retryToken = 0;
48
+ #resumePromise: Promise<void> | undefined;
49
+ #resumeResolve: (() => void) | undefined;
50
+
51
+ constructor(host: TtsrCoordinatorHost, manager: TtsrManager | undefined) {
52
+ this.#host = host;
53
+ this.#manager = manager;
54
+ }
55
+
56
+ /** Configured TTSR manager, when stream rules are enabled. */
57
+ get manager(): TtsrManager | undefined {
58
+ return this.#manager;
59
+ }
60
+
61
+ /** Whether a TTSR-triggered stream abort is awaiting its continuation. */
62
+ get abortPending(): boolean {
63
+ return this.#abortPending;
64
+ }
65
+
66
+ /** Current resume gate awaited by post-prompt recovery. */
67
+ get resumeGate(): Promise<void> | undefined {
68
+ return this.#resumePromise;
69
+ }
70
+
71
+ /** Resets stream buffers at turn start. */
72
+ onTurnStart(): void {
73
+ this.#manager?.resetBuffer();
74
+ }
75
+
76
+ /** Advances repeat-after-gap tracking at turn end. */
77
+ onTurnEnd(): void {
78
+ this.#manager?.incrementMessageCount();
79
+ }
80
+
81
+ /** Checks one streamed message update and reports whether TTSR consumed it by aborting. */
82
+ async checkMessageUpdate(event: AgentEvent): Promise<boolean> {
83
+ if (event.type !== "message_update" || !this.#manager?.hasRules()) return false;
84
+ const assistantEvent = event.assistantMessageEvent;
85
+ let matchContext: TtsrMatchContext | undefined;
86
+ let streamingToolCall: ToolCall | undefined;
87
+ if (assistantEvent.type === "text_delta") {
88
+ matchContext = { source: "text" };
89
+ } else if (assistantEvent.type === "thinking_delta") {
90
+ matchContext = { source: "thinking" };
91
+ } else if (assistantEvent.type === "toolcall_delta") {
92
+ streamingToolCall = this.#getStreamingToolCallBlock(event.message, assistantEvent.contentIndex);
93
+ matchContext = this.#getToolMatchContext(streamingToolCall, assistantEvent.contentIndex);
94
+ }
95
+ if (!matchContext || !("delta" in assistantEvent)) return false;
96
+ const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
97
+ const matches = this.#checkStream(assistantEvent.delta, matchContext, streamingToolCall);
98
+ if (matches.length > 0 && this.#handleMatches(matches, matchContext, targetMessageTimestamp)) return true;
99
+ // AST rules use the reconstructed edit/write snapshot and are awaited so
100
+ // the manager self-throttles native matching.
101
+ if (matchContext.source === "tool" && this.#manager.hasAstRules()) {
102
+ const astMatches = await this.#checkAstStream(matchContext, streamingToolCall);
103
+ if (astMatches.length > 0 && this.#handleMatches(astMatches, matchContext, targetMessageTimestamp))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /** Settles the previous resume gate and queues any deferred injection. */
110
+ onAssistantMessageEnd(message: AssistantMessage): void {
111
+ // Gate on abortPending, not stopReason: unrelated aborts have no TTSR continuation.
112
+ if (!this.#abortPending) this.resolveResume();
113
+ this.#queueDeferredInjectionIfNeeded(message);
114
+ }
115
+
116
+ /** Marks names persisted with a delivered TTSR injection as injected. */
117
+ markInjectedFromDetails(details: unknown): void {
118
+ if (!details || typeof details !== "object" || Array.isArray(details)) return;
119
+ const rules = "rules" in details ? details.rules : undefined;
120
+ if (!Array.isArray(rules)) return;
121
+ this.#markInjected(rules.filter((ruleName): ruleName is string => typeof ruleName === "string"));
122
+ }
123
+
124
+ /** Folds per-tool reminders into the matched tool's result. */
125
+ afterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
126
+ const rules = this.#perToolInjections.get(ctx.toolCall.id);
127
+ if (!rules || rules.length === 0) return undefined;
128
+ this.#perToolInjections.delete(ctx.toolCall.id);
129
+ const reminder = rules
130
+ .map(rule =>
131
+ prompt.render(ttsrToolReminderTemplate, {
132
+ name: rule.name,
133
+ path: this.#displayRulePath(rule.path),
134
+ content: rule.content,
135
+ }),
136
+ )
137
+ .join("\n\n");
138
+ const ruleNames = rules.map(rule => rule.name.trim()).filter(name => name.length > 0);
139
+ if (ruleNames.length > 0) this.#host.sessionManager.appendTtsrInjection(ruleNames);
140
+ return { content: [{ type: "text", text: reminder }, ...ctx.result.content] };
141
+ }
142
+
143
+ /** Resolves and clears the current resume gate. */
144
+ resolveResume(): void {
145
+ if (!this.#resumeResolve) return;
146
+ this.#resumeResolve();
147
+ this.#resumeResolve = undefined;
148
+ this.#resumePromise = undefined;
149
+ }
150
+
151
+ #ensureResumePromise(): void {
152
+ if (this.#resumePromise) return;
153
+ const { promise, resolve } = Promise.withResolvers<void>();
154
+ this.#resumePromise = promise;
155
+ this.#resumeResolve = resolve;
156
+ }
157
+
158
+ #formatAbortReason(rules: Rule[]): string {
159
+ const label = rules.length === 1 ? "rule" : "rules";
160
+ return `TTSR matched ${label}: ${rules.map(rule => rule.name).join(", ")}`;
161
+ }
162
+
163
+ #getInjectionContent(): { content: string; rules: Rule[] } | undefined {
164
+ if (this.#pendingInjections.length === 0) return undefined;
165
+ const rules = this.#pendingInjections;
166
+ const content = rules
167
+ .map(rule =>
168
+ prompt.render(ttsrInterruptTemplate, {
169
+ name: rule.name,
170
+ path: this.#displayRulePath(rule.path),
171
+ content: rule.content,
172
+ }),
173
+ )
174
+ .join("\n\n");
175
+ this.#pendingInjections = [];
176
+ return { content, rules };
177
+ }
178
+
179
+ #displayRulePath(rulePath: string): string {
180
+ const cwd = this.#host.sessionManager.getCwd();
181
+ const cwdRelative = relativePathWithinRoot(cwd, rulePath) ?? this.#displayPathWithinRoot(cwd, rulePath);
182
+ if (cwdRelative) return cwdRelative;
183
+ const homeRelative = relativePathWithinRoot(os.homedir(), rulePath);
184
+ if (homeRelative) return `~/${homeRelative}`;
185
+ return rulePath;
186
+ }
187
+
188
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
189
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
190
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
191
+ }
192
+
193
+ #addPendingInjections(rules: Rule[]): void {
194
+ const seen = new Set(this.#pendingInjections.map(rule => rule.name));
195
+ for (const rule of rules) {
196
+ if (seen.has(rule.name)) continue;
197
+ this.#pendingInjections.push(rule);
198
+ seen.add(rule.name);
199
+ }
200
+ }
201
+
202
+ #extractToolCallId(matchContext: TtsrMatchContext): string | undefined {
203
+ if (matchContext.source !== "tool") return undefined;
204
+ const key = matchContext.streamKey;
205
+ if (typeof key !== "string" || !key.startsWith("toolcall:")) return undefined;
206
+ const id = key.slice("toolcall:".length);
207
+ return id.length > 0 ? id : undefined;
208
+ }
209
+
210
+ #addPerToolInjections(toolCallId: string, rules: Rule[]): void {
211
+ const bucket = this.#perToolInjections.get(toolCallId) ?? [];
212
+ const seen = new Set(bucket.map(rule => rule.name));
213
+ const claimedElsewhere = new Set<string>();
214
+ for (const [otherId, otherBucket] of this.#perToolInjections) {
215
+ if (otherId === toolCallId) continue;
216
+ for (const rule of otherBucket) claimedElsewhere.add(rule.name);
217
+ }
218
+ const newlyAdded: string[] = [];
219
+ for (const rule of rules) {
220
+ if (seen.has(rule.name) || claimedElsewhere.has(rule.name)) continue;
221
+ bucket.push(rule);
222
+ seen.add(rule.name);
223
+ newlyAdded.push(rule.name);
224
+ }
225
+ if (bucket.length === 0) return;
226
+ this.#perToolInjections.set(toolCallId, bucket);
227
+ if (newlyAdded.length > 0) this.#manager?.markInjectedByNames(newlyAdded);
228
+ }
229
+
230
+ #markInjected(ruleNames: string[]): void {
231
+ const uniqueRuleNames = Array.from(
232
+ new Set(ruleNames.map(ruleName => ruleName.trim()).filter(ruleName => ruleName.length > 0)),
233
+ );
234
+ if (uniqueRuleNames.length === 0) return;
235
+ this.#manager?.markInjectedByNames(uniqueRuleNames);
236
+ this.#host.sessionManager.appendTtsrInjection(uniqueRuleNames);
237
+ }
238
+
239
+ #findAssistantIndex(targetTimestamp: number | undefined): number {
240
+ const messages = this.#host.agent.state.messages;
241
+ for (let index = messages.length - 1; index >= 0; index--) {
242
+ const message = messages[index];
243
+ if (message.role === "assistant" && (targetTimestamp === undefined || message.timestamp === targetTimestamp)) {
244
+ return index;
245
+ }
246
+ }
247
+ return -1;
248
+ }
249
+
250
+ #shouldInterrupt(matches: Rule[], matchContext: TtsrMatchContext): boolean {
251
+ const globalMode = this.#manager?.getSettings().interruptMode ?? "always";
252
+ for (const rule of matches) {
253
+ const mode = rule.interruptMode ?? globalMode;
254
+ if (mode === "never") continue;
255
+ if (mode === "prose-only" && (matchContext.source === "text" || matchContext.source === "thinking")) {
256
+ return true;
257
+ }
258
+ if (mode === "tool-only" && matchContext.source === "tool") return true;
259
+ if (mode === "always") return true;
260
+ }
261
+ return false;
262
+ }
263
+
264
+ #queueDeferredInjectionIfNeeded(message: AssistantMessage): void {
265
+ if (message.stopReason === "aborted" || message.stopReason === "error") this.#perToolInjections.clear();
266
+ if (this.#abortPending || this.#pendingInjections.length === 0) return;
267
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
268
+ this.#pendingInjections = [];
269
+ return;
270
+ }
271
+ const injection = this.#getInjectionContent();
272
+ if (!injection) return;
273
+ this.#host.agent.followUp({
274
+ role: "custom",
275
+ customType: "ttsr-injection",
276
+ content: injection.content,
277
+ display: false,
278
+ details: { rules: injection.rules.map(rule => rule.name) },
279
+ attribution: "agent",
280
+ timestamp: Date.now(),
281
+ });
282
+ this.#ensureResumePromise();
283
+ this.#host.scheduleAgentContinue({
284
+ delayMs: 1,
285
+ generation: this.#host.promptGeneration(),
286
+ onSkip: () => this.resolveResume(),
287
+ shouldContinue: () => {
288
+ if (this.#host.agent.state.isStreaming || !this.#host.agent.hasQueuedMessages()) {
289
+ this.resolveResume();
290
+ return false;
291
+ }
292
+ return true;
293
+ },
294
+ onError: () => this.resolveResume(),
295
+ });
296
+ }
297
+
298
+ #getStreamingToolCallBlock(message: AgentMessage, contentIndex: number): ToolCall | undefined {
299
+ if (message.role !== "assistant") return undefined;
300
+ const content = message.content;
301
+ if (!Array.isArray(content) || contentIndex < 0 || contentIndex >= content.length) return undefined;
302
+ const block = content[contentIndex];
303
+ return block && typeof block === "object" && block.type === "toolCall" ? (block as ToolCall) : undefined;
304
+ }
305
+
306
+ #getToolMatchContext(toolCall: ToolCall | undefined, contentIndex: number): TtsrMatchContext {
307
+ const context: TtsrMatchContext = { source: "tool" };
308
+ if (!toolCall) return context;
309
+ context.toolName = toolCall.name;
310
+ context.streamKey = toolCall.id ? `toolcall:${toolCall.id}` : `tool:${toolCall.name}:${contentIndex}`;
311
+ context.filePaths = this.#extractToolFilePaths(toolCall);
312
+ return context;
313
+ }
314
+
315
+ #extractToolFilePaths(toolCall: ToolCall): string[] | undefined {
316
+ const args = toolCall.arguments ?? {};
317
+ const tool = this.#resolveTool(toolCall);
318
+ const toolPaths = tool?.matcherPaths?.(args);
319
+ if (toolPaths && toolPaths.length > 0) {
320
+ const normalized = toolPaths.flatMap(filePath => this.#normalizePathCandidates(filePath));
321
+ if (normalized.length > 0) return Array.from(new Set(normalized));
322
+ }
323
+ return this.#extractFilePathsFromArgs(args);
324
+ }
325
+
326
+ #checkStream(delta: string, matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Rule[] {
327
+ if (!this.#manager) return [];
328
+ const entries = this.#resolveMatcherEntries(toolCall);
329
+ if (entries) {
330
+ const matches: Rule[] = [];
331
+ for (const entry of entries) {
332
+ matches.push(...this.#manager.checkSnapshot(entry.digest, this.#perFileContext(matchContext, entry.path)));
333
+ }
334
+ return matches;
335
+ }
336
+ const digest = this.#resolveMatcherDigest(toolCall);
337
+ return digest !== undefined
338
+ ? this.#manager.checkSnapshot(digest, matchContext)
339
+ : this.#manager.checkDelta(delta, matchContext);
340
+ }
341
+
342
+ #resolveMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
343
+ const tool = this.#resolveTool(toolCall);
344
+ return tool?.matcherDigest?.(toolCall?.arguments ?? {});
345
+ }
346
+
347
+ #resolveMatcherEntries(toolCall: ToolCall | undefined): readonly { path: string; digest: string }[] | undefined {
348
+ const tool = this.#resolveTool(toolCall);
349
+ const entries = tool?.matcherEntries?.(toolCall?.arguments ?? {});
350
+ return entries && entries.length > 0 ? entries : undefined;
351
+ }
352
+
353
+ #resolveTool(toolCall: ToolCall | undefined) {
354
+ if (!toolCall) return undefined;
355
+ const tools = this.#host.agent.state.tools;
356
+ return (
357
+ tools.find(tool => tool.name === toolCall.name) ??
358
+ tools.find(tool => tool.customWireName !== undefined && tool.customWireName === toolCall.name)
359
+ );
360
+ }
361
+
362
+ #perFileContext(base: TtsrMatchContext, filePath: string): TtsrMatchContext {
363
+ const filePaths = this.#normalizePathCandidates(filePath);
364
+ return {
365
+ ...base,
366
+ filePaths: filePaths.length > 0 ? filePaths : [filePath],
367
+ streamKey: base.streamKey ? `${base.streamKey}#${filePath}` : undefined,
368
+ };
369
+ }
370
+
371
+ async #checkAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
372
+ if (!this.#manager) return [];
373
+ const entries = this.#resolveMatcherEntries(toolCall);
374
+ if (entries) {
375
+ const matches: Rule[] = [];
376
+ for (const entry of entries) {
377
+ matches.push(
378
+ ...(await this.#manager.checkAstSnapshot(entry.digest, this.#perFileContext(matchContext, entry.path))),
379
+ );
380
+ }
381
+ return matches;
382
+ }
383
+ const digest = this.#resolveMatcherDigest(toolCall);
384
+ return digest === undefined ? [] : this.#manager.checkAstSnapshot(digest, matchContext);
385
+ }
386
+
387
+ #handleMatches(matches: Rule[], matchContext: TtsrMatchContext, targetTimestamp: number | undefined): boolean {
388
+ const shouldInterrupt = this.#shouldInterrupt(matches, matchContext);
389
+ const matchedToolId = this.#extractToolCallId(matchContext);
390
+ const perToolId = shouldInterrupt ? undefined : matchedToolId;
391
+ if (perToolId) {
392
+ this.#addPerToolInjections(perToolId, matches);
393
+ this.#host.emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
394
+ return false;
395
+ }
396
+ this.#addPendingInjections(matches);
397
+ if (!shouldInterrupt) return false;
398
+
399
+ this.#abortPending = true;
400
+ this.#ensureResumePromise();
401
+ const abortReason = this.#formatAbortReason(matches);
402
+ this.#host.agent.abort(
403
+ matchedToolId
404
+ ? createToolScopedAbortReason(
405
+ abortReason,
406
+ { [matchedToolId]: abortReason },
407
+ "TTSR interrupt on another tool call",
408
+ )
409
+ : abortReason,
410
+ );
411
+ this.#host.emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
412
+ const retryToken = ++this.#retryToken;
413
+ const generation = this.#host.promptGeneration();
414
+ this.#host.schedulePostPromptTask(
415
+ async () => {
416
+ if (this.#retryToken !== retryToken) {
417
+ this.resolveResume();
418
+ return;
419
+ }
420
+ const targetAssistantIndex = this.#findAssistantIndex(targetTimestamp);
421
+ if (!this.#abortPending || this.#host.promptGeneration() !== generation || targetAssistantIndex === -1) {
422
+ this.#abortPending = false;
423
+ this.#pendingInjections = [];
424
+ this.#perToolInjections.clear();
425
+ this.resolveResume();
426
+ return;
427
+ }
428
+ this.#abortPending = false;
429
+ this.#perToolInjections.clear();
430
+ if (this.#manager?.getSettings().contextMode === "discard") {
431
+ this.#host.agent.replaceMessages(this.#host.agent.state.messages.slice(0, targetAssistantIndex));
432
+ }
433
+ const injection = this.#getInjectionContent();
434
+ if (injection) {
435
+ const details = { rules: injection.rules.map(rule => rule.name) };
436
+ this.#host.agent.appendMessage({
437
+ role: "custom",
438
+ customType: "ttsr-injection",
439
+ content: injection.content,
440
+ display: false,
441
+ details,
442
+ attribution: "agent",
443
+ timestamp: Date.now(),
444
+ });
445
+ this.#host.sessionManager.appendCustomMessageEntry(
446
+ "ttsr-injection",
447
+ injection.content,
448
+ false,
449
+ details,
450
+ "agent",
451
+ );
452
+ this.#markInjected(details.rules);
453
+ }
454
+ try {
455
+ await this.#host.agent.continue();
456
+ } catch {
457
+ this.resolveResume();
458
+ }
459
+ },
460
+ { delayMs: 50 },
461
+ );
462
+ return true;
463
+ }
464
+
465
+ #extractFilePathsFromArgs(args: unknown): string[] | undefined {
466
+ if (!isRecord(args)) return undefined;
467
+ const rawPaths: string[] = [];
468
+ for (const key in args) {
469
+ const value = args[key];
470
+ const normalizedKey = key.toLowerCase();
471
+ if (typeof value === "string" && (normalizedKey === "path" || normalizedKey.endsWith("path"))) {
472
+ rawPaths.push(value);
473
+ continue;
474
+ }
475
+ if (Array.isArray(value) && (normalizedKey === "paths" || normalizedKey.endsWith("paths"))) {
476
+ for (const candidate of value) if (typeof candidate === "string") rawPaths.push(candidate);
477
+ }
478
+ }
479
+ const normalizedPaths = rawPaths.flatMap(filePath => this.#normalizePathCandidates(filePath));
480
+ return normalizedPaths.length === 0 ? undefined : Array.from(new Set(normalizedPaths));
481
+ }
482
+
483
+ #normalizePathCandidates(rawPath: string): string[] {
484
+ const trimmed = rawPath.trim();
485
+ if (trimmed.length === 0) return [];
486
+ const normalizedInput = trimmed.replaceAll("\\", "/");
487
+ const candidates = new Set<string>([normalizedInput]);
488
+ if (normalizedInput.startsWith("./")) candidates.add(normalizedInput.slice(2));
489
+ const cwd = this.#host.sessionManager.getCwd();
490
+ const absolutePath = path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.resolve(cwd, trimmed);
491
+ candidates.add(absolutePath.replaceAll("\\", "/"));
492
+ const relative = path.relative(cwd, absolutePath).replaceAll("\\", "/");
493
+ if (relative && relative !== "." && !relative.startsWith("../") && relative !== "..") candidates.add(relative);
494
+ return Array.from(candidates);
495
+ }
496
+ }