@oh-my-pi/pi-coding-agent 17.0.9 → 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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  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/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -0,0 +1,380 @@
1
+ import type { Agent, AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Message, Model, TextContent, ToolChoice } from "@oh-my-pi/pi-ai";
3
+ import { isRecord, logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
4
+ import type { Settings } from "../config/settings";
5
+ import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
6
+ import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
7
+ import midRunTodoNudgePrompt from "../prompts/system/mid-run-todo-nudge.md" with { type: "text" };
8
+ import { getLatestTodoPhasesFromEntries, isTodoPhase, type TodoItem, type TodoPhase } from "../tools/todo";
9
+ import { buildNamedToolChoice } from "../utils/tool-choice";
10
+ import type { AgentSessionEvent } from "./agent-session-events";
11
+ import type { SessionManager } from "./session-manager";
12
+
13
+ const MID_RUN_NUDGE_MUTATION_THRESHOLD = 12;
14
+ const MID_RUN_NUDGE_MAX_PER_CYCLE = 2;
15
+ const MUTATING_TOOLS: Record<string, true> = {
16
+ bash: true,
17
+ eval: true,
18
+ edit: true,
19
+ write: true,
20
+ ast_edit: true,
21
+ };
22
+ const MID_RUN_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
23
+ const MARKDOWN_PROMPT_PREFIX_RE = /^(?:>\s*)?(?:(?:[-*+]|\d+[.)])\s+)*/;
24
+ const PROMPT_LABEL_RE = /^(?:q(?:uestion)?|ask)\s*\d*\s*[:.)-]\s*/i;
25
+ const QUESTION_PROMPT_RE =
26
+ /^(?:what|which|when|where|why|how|who|whom|whose|do|does|did|can|could|would|will|should|is|are|am|may|shall)\b/i;
27
+ const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
28
+ const USER_RESPONSE_CUE_RE =
29
+ /^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
30
+
31
+ interface PromptLine {
32
+ text: string;
33
+ hadPromptLabel: boolean;
34
+ }
35
+
36
+ /** Capabilities the todo tracker borrows from its owning session. */
37
+ export interface TodoTrackerHost {
38
+ agent: Agent;
39
+ sessionManager: SessionManager;
40
+ settings: Settings;
41
+ model(): Model | undefined;
42
+ agentKind(): "main" | "sub";
43
+ emitSessionEvent(event: AgentSessionEvent): Promise<void>;
44
+ scheduleAgentContinue(options: { generation?: number }): void;
45
+ promptGeneration(): number;
46
+ hasPendingAsyncWake(): boolean;
47
+ getActiveToolNames(): string[];
48
+ toolRegistry(): Map<string, AgentTool>;
49
+ planModeEnabled(): boolean;
50
+ consumeLastServedToolChoiceLabel(): string | undefined;
51
+ }
52
+
53
+ /** Owns canonical todo state, eager preludes, and completion reminders. */
54
+ export class TodoTracker {
55
+ readonly #host: TodoTrackerHost;
56
+ #phases: TodoPhase[] = [];
57
+ #reminderCount = 0;
58
+ #reminderAwaitingProgress = false;
59
+ #mutationsSinceLastTouch = 0;
60
+ #midRunNudgeCount = 0;
61
+
62
+ constructor(host: TodoTrackerHost) {
63
+ this.#host = host;
64
+ }
65
+
66
+ /** Returns a defensive clone of the current todo phases. */
67
+ get phases(): TodoPhase[] {
68
+ return this.#clonePhases(this.#phases);
69
+ }
70
+
71
+ /** Replaces todo phases with a defensive clone. */
72
+ setPhases(phases: TodoPhase[]): void {
73
+ this.#phases = this.#clonePhases(phases);
74
+ }
75
+
76
+ /** Rehydrates todo phases from the current transcript branch. */
77
+ syncFromBranch(): void {
78
+ this.setPhases(getLatestTodoPhasesFromEntries(this.#host.sessionManager.getBranch()));
79
+ }
80
+
81
+ /** Returns a defensive clone suitable for snapshots and branch state. */
82
+ clonePhases(phases: TodoPhase[]): TodoPhase[] {
83
+ return this.#clonePhases(phases);
84
+ }
85
+
86
+ /** Resets per-prompt reminder and mutation budgets. */
87
+ resetCycle(): void {
88
+ this.#reminderCount = 0;
89
+ this.#reminderAwaitingProgress = false;
90
+ this.#mutationsSinceLastTouch = 0;
91
+ this.#midRunNudgeCount = 0;
92
+ }
93
+
94
+ /** Records a completed tool result before asynchronous event processing begins. */
95
+ onToolResult(toolName: string, isError: boolean): void {
96
+ if (toolName === "todo") {
97
+ this.#mutationsSinceLastTouch = 0;
98
+ } else if (!isError && MUTATING_TOOLS[toolName]) {
99
+ this.#mutationsSinceLastTouch++;
100
+ }
101
+ this.#reminderAwaitingProgress = false;
102
+ }
103
+
104
+ /** Detects whether a successful todo result came from an init operation. */
105
+ onTodoResultDetails(details: Record<string, unknown>, toolCallId: string | undefined): boolean {
106
+ const phases = details.phases;
107
+ if (!Array.isArray(phases) || !phases.every(isTodoPhase)) return false;
108
+ const detailOp = stringProperty(details, "op");
109
+ if (detailOp) return detailOp === "init";
110
+ if (!toolCallId) return false;
111
+ for (let index = this.#host.agent.state.messages.length - 1; index >= 0; index--) {
112
+ const message = this.#host.agent.state.messages[index];
113
+ if (!message) continue;
114
+ const op = toolCallOpFromMessage(message, toolCallId);
115
+ if (op) return op === "init";
116
+ }
117
+ return false;
118
+ }
119
+
120
+ /** Builds the first-turn eager todo prelude and optional forced tool choice. */
121
+ createEagerTodoPrelude(
122
+ promptText: string | undefined,
123
+ ): { message: AgentMessage; toolChoice?: ToolChoice } | undefined {
124
+ const mode = this.#host.settings.get("todo.eager");
125
+ if (mode === "default" || !this.#host.settings.get("todo.enabled")) return undefined;
126
+ if (this.#host.planModeEnabled() || this.#phases.length > 0) return undefined;
127
+ if (promptText !== undefined) {
128
+ if (this.#host.agent.state.messages.some(message => message.role === "user")) return undefined;
129
+ const trimmedPromptText = promptText.trimEnd();
130
+ if (trimmedPromptText.endsWith("?") || trimmedPromptText.endsWith("!")) return undefined;
131
+ }
132
+ const activeToolNames = this.#host.getActiveToolNames();
133
+ if (!activeToolNames.includes("todo")) {
134
+ logger.warn("Eager todo enforcement skipped because todo is not active", { activeToolNames });
135
+ return undefined;
136
+ }
137
+ const message: AgentMessage = {
138
+ role: "custom",
139
+ customType: "eager-todo-prelude",
140
+ content: prompt.render(eagerTodoPrompt, { ...this.#buildEagerPreludeContext(), forced: mode === "always" }),
141
+ display: false,
142
+ attribution: "agent",
143
+ timestamp: Date.now(),
144
+ };
145
+ if (promptText === undefined || mode === "preferred") return { message };
146
+ const model = this.#host.model();
147
+ const toolChoice = buildNamedToolChoice("todo", model);
148
+ if (!toolChoice) {
149
+ logger.warn(
150
+ "Eager todo proceeding with the reminder only because the current model does not support a forced todo tool_choice",
151
+ { modelApi: model?.api, modelId: model?.id },
152
+ );
153
+ return { message };
154
+ }
155
+ return { message, toolChoice };
156
+ }
157
+
158
+ /** Builds the first-turn eager task-delegation prelude. */
159
+ createEagerTaskPrelude(promptText: string | undefined): AgentMessage | undefined {
160
+ if (this.#host.settings.get("task.eager") !== "always") return undefined;
161
+ if (this.#host.agentKind() === "sub" || this.#host.planModeEnabled()) return undefined;
162
+ if (promptText !== undefined) {
163
+ if (this.#host.agent.state.messages.some(message => message.role === "user")) return undefined;
164
+ const trimmed = promptText.trimEnd();
165
+ if (trimmed.endsWith("?") || trimmed.endsWith("!")) return undefined;
166
+ }
167
+ if (!this.#host.getActiveToolNames().includes("task")) return undefined;
168
+ return {
169
+ role: "custom",
170
+ customType: "eager-task-prelude",
171
+ content: prompt.render(eagerTaskPrompt, this.#buildEagerPreludeContext()),
172
+ display: false,
173
+ attribution: "agent",
174
+ timestamp: Date.now(),
175
+ };
176
+ }
177
+
178
+ /** Builds reminder-only eager preludes after compaction. */
179
+ buildPostCompactionEagerNudges(): AgentMessage[] {
180
+ const nudges: AgentMessage[] = [];
181
+ const todo = this.createEagerTodoPrelude(undefined);
182
+ if (todo) nudges.push(todo.message);
183
+ const task = this.createEagerTaskPrelude(undefined);
184
+ if (task) nudges.push(task);
185
+ return nudges;
186
+ }
187
+
188
+ /** Checks a terminal assistant turn and schedules continuation for incomplete todos. */
189
+ async checkCompletion(message: AssistantMessage): Promise<boolean> {
190
+ if (this.#host.consumeLastServedToolChoiceLabel() === "user-force") return false;
191
+ if (this.#host.planModeEnabled()) return false;
192
+ if (this.#reminderAwaitingProgress) {
193
+ logger.debug("Todo completion: prior reminder still awaiting agent action; staying silent", {
194
+ attempt: this.#reminderCount,
195
+ });
196
+ return false;
197
+ }
198
+ if (!this.#host.settings.get("todo.reminders") || !this.#host.settings.get("todo.enabled")) {
199
+ this.#reminderCount = 0;
200
+ this.#reminderAwaitingProgress = false;
201
+ return false;
202
+ }
203
+ const remindersMax = this.#host.settings.get("todo.remindersMax");
204
+ if (this.#reminderCount >= remindersMax) {
205
+ logger.debug("Todo completion: max reminders reached", { count: this.#reminderCount });
206
+ return false;
207
+ }
208
+ const phases = this.phases;
209
+ if (phases.length === 0) {
210
+ this.#reminderCount = 0;
211
+ this.#reminderAwaitingProgress = false;
212
+ return false;
213
+ }
214
+ const incompleteByPhase = phases
215
+ .map(phase => ({
216
+ name: phase.name,
217
+ tasks: phase.tasks
218
+ .filter(
219
+ (task): task is TodoItem & { status: "pending" | "in_progress" } =>
220
+ task.status === "pending" || task.status === "in_progress",
221
+ )
222
+ .map(task => ({ content: task.content, status: task.status })),
223
+ }))
224
+ .filter(phase => phase.tasks.length > 0);
225
+ const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
226
+ if (incomplete.length === 0) {
227
+ this.#reminderCount = 0;
228
+ this.#reminderAwaitingProgress = false;
229
+ return false;
230
+ }
231
+ if (isAwaitingUserAnswer(message)) {
232
+ logger.debug("Todo completion: assistant is waiting for user input; skipping reminder", {
233
+ incomplete: incomplete.length,
234
+ });
235
+ return false;
236
+ }
237
+ if (this.#host.hasPendingAsyncWake()) {
238
+ logger.debug("Todo completion: async jobs in flight will re-wake the loop; skipping reminder", {
239
+ incomplete: incomplete.length,
240
+ });
241
+ return false;
242
+ }
243
+ this.#reminderCount++;
244
+ const todoList = incompleteByPhase
245
+ .map(phase => `- ${phase.name}\n${phase.tasks.map(task => ` - ${task.content}`).join("\n")}`)
246
+ .join("\n");
247
+ const reminder =
248
+ `<system-reminder>\n` +
249
+ `You stopped with ${incomplete.length} incomplete todo item(s):\n${todoList}\n\n` +
250
+ `Please continue working on these tasks or mark them complete if finished.\n` +
251
+ `(Reminder ${this.#reminderCount}/${remindersMax})\n` +
252
+ `</system-reminder>`;
253
+ logger.debug("Todo completion: sending reminder", {
254
+ incomplete: incomplete.length,
255
+ attempt: this.#reminderCount,
256
+ });
257
+ await this.#host.emitSessionEvent({
258
+ type: "todo_reminder",
259
+ todos: incomplete,
260
+ attempt: this.#reminderCount,
261
+ maxAttempts: remindersMax,
262
+ });
263
+ const reminderMessage: Message = {
264
+ role: "developer",
265
+ content: [{ type: "text", text: reminder }],
266
+ attribution: "agent",
267
+ timestamp: Date.now(),
268
+ };
269
+ this.#mutationsSinceLastTouch = 0;
270
+ this.#reminderAwaitingProgress = true;
271
+ this.#host.agent.appendMessage(reminderMessage);
272
+ this.#host.sessionManager.appendMessage(reminderMessage);
273
+ this.#host.scheduleAgentContinue({ generation: this.#host.promptGeneration() });
274
+ return true;
275
+ }
276
+
277
+ /** Takes the next hidden mid-run reconciliation nudge, if its budget and guards allow. */
278
+ takeMidRunNudge(): AgentMessage | null {
279
+ if (this.#mutationsSinceLastTouch < MID_RUN_NUDGE_MUTATION_THRESHOLD) return null;
280
+ if (this.#midRunNudgeCount >= MID_RUN_NUDGE_MAX_PER_CYCLE) return null;
281
+ if (!this.#host.settings.get("todo.enabled") || !this.#host.settings.get("todo.reminders")) return null;
282
+ if (this.#host.planModeEnabled() || !this.#host.getActiveToolNames().includes("todo")) return null;
283
+ const incomplete = this.#phases
284
+ .flatMap(phase => phase.tasks)
285
+ .filter(task => task.status === "pending" || task.status === "in_progress");
286
+ if (incomplete.length === 0) return null;
287
+ this.#mutationsSinceLastTouch = 0;
288
+ this.#midRunNudgeCount++;
289
+ const { toolRefs } = this.#buildEagerPreludeContext();
290
+ const reminder = prompt.render(midRunTodoNudgePrompt, {
291
+ toolRefs,
292
+ incompleteCount: incomplete.length,
293
+ plural: incomplete.length !== 1,
294
+ });
295
+ logger.debug("Mid-run todo nudge fired", {
296
+ incomplete: incomplete.length,
297
+ nudge: this.#midRunNudgeCount,
298
+ });
299
+ return {
300
+ role: "custom",
301
+ customType: MID_RUN_NUDGE_MESSAGE_TYPE,
302
+ content: reminder,
303
+ display: false,
304
+ attribution: "agent",
305
+ timestamp: Date.now(),
306
+ };
307
+ }
308
+
309
+ #buildEagerPreludeContext(): { toolRefs: Record<string, string>; taskBatch: boolean } {
310
+ const wireName = (name: string): string => {
311
+ const tool = this.#host.toolRegistry().get(name);
312
+ return typeof tool?.customWireName === "string" ? tool.customWireName : name;
313
+ };
314
+ return {
315
+ toolRefs: { task: wireName("task"), todo: wireName("todo") },
316
+ taskBatch: this.#host.settings.get("task.batch"),
317
+ };
318
+ }
319
+
320
+ #clonePhases(phases: TodoPhase[]): TodoPhase[] {
321
+ return phases.map(phase => ({
322
+ name: phase.name,
323
+ tasks: phase.tasks.map(task =>
324
+ task.blocker !== undefined
325
+ ? { content: task.content, status: task.status, blocker: task.blocker }
326
+ : { content: task.content, status: task.status },
327
+ ),
328
+ }));
329
+ }
330
+ }
331
+
332
+ function toolCallOpFromMessage(message: AgentMessage, toolCallId: string): string | undefined {
333
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return undefined;
334
+ for (const block of message.content) {
335
+ if (!isRecord(block) || block.type !== "toolCall" || block.id !== toolCallId) continue;
336
+ return isRecord(block.arguments) ? stringProperty(block.arguments, "op") : undefined;
337
+ }
338
+ return undefined;
339
+ }
340
+
341
+ function assistantText(message: AssistantMessage): string {
342
+ return message.content
343
+ .filter((content): content is TextContent => content.type === "text")
344
+ .map(content => content.text)
345
+ .join("\n")
346
+ .trim();
347
+ }
348
+
349
+ function promptLine(line: string): PromptLine {
350
+ const withoutMarkdownPrefix = line.trim().replace(MARKDOWN_PROMPT_PREFIX_RE, "").trim();
351
+ const withoutPromptLabel = withoutMarkdownPrefix.replace(PROMPT_LABEL_RE, "").trim();
352
+ return {
353
+ text: withoutPromptLabel,
354
+ hadPromptLabel: withoutPromptLabel !== withoutMarkdownPrefix,
355
+ };
356
+ }
357
+
358
+ function isQuestionPromptLine(line: string): boolean {
359
+ const candidate = promptLine(line);
360
+ if (!/[??]\s*$/.test(candidate.text)) return false;
361
+ return (
362
+ candidate.hadPromptLabel ||
363
+ QUESTION_PROMPT_RE.test(candidate.text) ||
364
+ USER_DIRECTED_PROMPT_RE.test(candidate.text)
365
+ );
366
+ }
367
+
368
+ function isResponseCueLine(line: string): boolean {
369
+ const candidate = promptLine(line)
370
+ .text.replace(/[.!?。!?]+$/, "")
371
+ .trim();
372
+ return USER_RESPONSE_CUE_RE.test(candidate);
373
+ }
374
+
375
+ function isAwaitingUserAnswer(message: AssistantMessage): boolean {
376
+ const text = assistantText(message);
377
+ if (!text) return false;
378
+ const lastLine = text.split(/\r?\n/).at(-1)?.trim();
379
+ return lastLine !== undefined && (isQuestionPromptLine(lastLine) || isResponseCueLine(lastLine));
380
+ }