pulseed 0.5.3 → 0.5.4

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 (512) hide show
  1. package/dist/base/state/state-manager-goal-state.d.ts +40 -0
  2. package/dist/base/state/state-manager-goal-state.d.ts.map +1 -0
  3. package/dist/base/state/state-manager-goal-state.js +235 -0
  4. package/dist/base/state/state-manager-goal-state.js.map +1 -0
  5. package/dist/base/state/state-manager.d.ts +0 -5
  6. package/dist/base/state/state-manager.d.ts.map +1 -1
  7. package/dist/base/state/state-manager.js +27 -273
  8. package/dist/base/state/state-manager.js.map +1 -1
  9. package/dist/base/types/goal-activation.d.ts +25 -0
  10. package/dist/base/types/goal-activation.d.ts.map +1 -0
  11. package/dist/base/types/goal-activation.js +9 -0
  12. package/dist/base/types/goal-activation.js.map +1 -0
  13. package/dist/interface/chat/chat-history.d.ts +129 -0
  14. package/dist/interface/chat/chat-history.d.ts.map +1 -1
  15. package/dist/interface/chat/chat-history.js +135 -0
  16. package/dist/interface/chat/chat-history.js.map +1 -1
  17. package/dist/interface/chat/chat-runner-command-helpers.d.ts +50 -0
  18. package/dist/interface/chat/chat-runner-command-helpers.d.ts.map +1 -0
  19. package/dist/interface/chat/chat-runner-command-helpers.js +189 -0
  20. package/dist/interface/chat/chat-runner-command-helpers.js.map +1 -0
  21. package/dist/interface/chat/chat-runner-commands.d.ts +76 -0
  22. package/dist/interface/chat/chat-runner-commands.d.ts.map +1 -0
  23. package/dist/interface/chat/chat-runner-commands.js +814 -0
  24. package/dist/interface/chat/chat-runner-commands.js.map +1 -0
  25. package/dist/interface/chat/chat-runner-event-bridge.d.ts +53 -0
  26. package/dist/interface/chat/chat-runner-event-bridge.d.ts.map +1 -0
  27. package/dist/interface/chat/chat-runner-event-bridge.js +311 -0
  28. package/dist/interface/chat/chat-runner-event-bridge.js.map +1 -0
  29. package/dist/interface/chat/chat-runner-routes.d.ts +67 -0
  30. package/dist/interface/chat/chat-runner-routes.d.ts.map +1 -0
  31. package/dist/interface/chat/chat-runner-routes.js +594 -0
  32. package/dist/interface/chat/chat-runner-routes.js.map +1 -0
  33. package/dist/interface/chat/chat-runner-runtime.d.ts +37 -0
  34. package/dist/interface/chat/chat-runner-runtime.d.ts.map +1 -0
  35. package/dist/interface/chat/chat-runner-runtime.js +236 -0
  36. package/dist/interface/chat/chat-runner-runtime.js.map +1 -0
  37. package/dist/interface/chat/chat-runner-state.d.ts +20 -0
  38. package/dist/interface/chat/chat-runner-state.d.ts.map +1 -0
  39. package/dist/interface/chat/chat-runner-state.js +157 -0
  40. package/dist/interface/chat/chat-runner-state.js.map +1 -0
  41. package/dist/interface/chat/chat-runner-support.d.ts +15 -0
  42. package/dist/interface/chat/chat-runner-support.d.ts.map +1 -0
  43. package/dist/interface/chat/chat-runner-support.js +116 -0
  44. package/dist/interface/chat/chat-runner-support.js.map +1 -0
  45. package/dist/interface/chat/chat-runner.d.ts +10 -153
  46. package/dist/interface/chat/chat-runner.d.ts.map +1 -1
  47. package/dist/interface/chat/chat-runner.js +299 -2642
  48. package/dist/interface/chat/chat-runner.js.map +1 -1
  49. package/dist/interface/chat/chat-session-store.d.ts +32 -0
  50. package/dist/interface/chat/chat-session-store.d.ts.map +1 -1
  51. package/dist/interface/chat/chat-session-store.js +100 -10
  52. package/dist/interface/chat/chat-session-store.js.map +1 -1
  53. package/dist/interface/chat/cross-platform-session.d.ts.map +1 -1
  54. package/dist/interface/chat/cross-platform-session.js +2 -5
  55. package/dist/interface/chat/cross-platform-session.js.map +1 -1
  56. package/dist/interface/chat/event-subscriber.d.ts.map +1 -1
  57. package/dist/interface/chat/event-subscriber.js +41 -0
  58. package/dist/interface/chat/event-subscriber.js.map +1 -1
  59. package/dist/interface/chat/ingress-router.d.ts +0 -25
  60. package/dist/interface/chat/ingress-router.d.ts.map +1 -1
  61. package/dist/interface/chat/ingress-router.js +12 -85
  62. package/dist/interface/chat/ingress-router.js.map +1 -1
  63. package/dist/interface/chat/tend-command.d.ts.map +1 -1
  64. package/dist/interface/chat/tend-command.js +2 -0
  65. package/dist/interface/chat/tend-command.js.map +1 -1
  66. package/dist/interface/cli/commands/daemon-shared.d.ts +32 -0
  67. package/dist/interface/cli/commands/daemon-shared.d.ts.map +1 -0
  68. package/dist/interface/cli/commands/daemon-shared.js +120 -0
  69. package/dist/interface/cli/commands/daemon-shared.js.map +1 -0
  70. package/dist/interface/cli/commands/daemon.d.ts +2 -2
  71. package/dist/interface/cli/commands/daemon.d.ts.map +1 -1
  72. package/dist/interface/cli/commands/daemon.js +4 -120
  73. package/dist/interface/cli/commands/daemon.js.map +1 -1
  74. package/dist/interface/cli/commands/schedule/history.d.ts.map +1 -1
  75. package/dist/interface/cli/commands/schedule/history.js +5 -1
  76. package/dist/interface/cli/commands/schedule/history.js.map +1 -1
  77. package/dist/interface/cli/commands/schedule.js +34 -4
  78. package/dist/interface/cli/commands/schedule.js.map +1 -1
  79. package/dist/interface/tui/app.d.ts.map +1 -1
  80. package/dist/interface/tui/app.js +14 -1
  81. package/dist/interface/tui/app.js.map +1 -1
  82. package/dist/interface/tui/chat/suggestions.d.ts.map +1 -1
  83. package/dist/interface/tui/chat/suggestions.js +118 -4
  84. package/dist/interface/tui/chat/suggestions.js.map +1 -1
  85. package/dist/interface/tui/entry-approval.d.ts +8 -0
  86. package/dist/interface/tui/entry-approval.d.ts.map +1 -0
  87. package/dist/interface/tui/entry-approval.js +59 -0
  88. package/dist/interface/tui/entry-approval.js.map +1 -0
  89. package/dist/interface/tui/entry-daemon.d.ts +12 -0
  90. package/dist/interface/tui/entry-daemon.d.ts.map +1 -0
  91. package/dist/interface/tui/entry-daemon.js +74 -0
  92. package/dist/interface/tui/entry-daemon.js.map +1 -0
  93. package/dist/interface/tui/entry-deps.d.ts +22 -0
  94. package/dist/interface/tui/entry-deps.d.ts.map +1 -0
  95. package/dist/interface/tui/entry-deps.js +409 -0
  96. package/dist/interface/tui/entry-deps.js.map +1 -0
  97. package/dist/interface/tui/entry.d.ts +2 -4
  98. package/dist/interface/tui/entry.d.ts.map +1 -1
  99. package/dist/interface/tui/entry.js +10 -557
  100. package/dist/interface/tui/entry.js.map +1 -1
  101. package/dist/interface/tui/fullscreen-chat-render.d.ts +127 -0
  102. package/dist/interface/tui/fullscreen-chat-render.d.ts.map +1 -0
  103. package/dist/interface/tui/fullscreen-chat-render.js +667 -0
  104. package/dist/interface/tui/fullscreen-chat-render.js.map +1 -0
  105. package/dist/interface/tui/fullscreen-chat.d.ts +2 -111
  106. package/dist/interface/tui/fullscreen-chat.d.ts.map +1 -1
  107. package/dist/interface/tui/fullscreen-chat.js +4 -663
  108. package/dist/interface/tui/fullscreen-chat.js.map +1 -1
  109. package/dist/interface/tui/help-overlay.d.ts.map +1 -1
  110. package/dist/interface/tui/help-overlay.js +1 -1
  111. package/dist/interface/tui/help-overlay.js.map +1 -1
  112. package/dist/interface/tui/intent-recognizer.js +2 -2
  113. package/dist/interface/tui/intent-recognizer.js.map +1 -1
  114. package/dist/orchestrator/execution/agent-loop/agent-loop-default-profile.d.ts.map +1 -1
  115. package/dist/orchestrator/execution/agent-loop/agent-loop-default-profile.js +8 -0
  116. package/dist/orchestrator/execution/agent-loop/agent-loop-default-profile.js.map +1 -1
  117. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-router.d.ts.map +1 -1
  118. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-router.js +2 -1
  119. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-router.js.map +1 -1
  120. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-runtime.d.ts +2 -0
  121. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-runtime.d.ts.map +1 -1
  122. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-runtime.js +31 -0
  123. package/dist/orchestrator/execution/agent-loop/agent-loop-tool-runtime.js.map +1 -1
  124. package/dist/orchestrator/execution/agent-loop/agent-loop-turn-context.d.ts +1 -0
  125. package/dist/orchestrator/execution/agent-loop/agent-loop-turn-context.d.ts.map +1 -1
  126. package/dist/orchestrator/execution/agent-loop/agent-loop-turn-context.js.map +1 -1
  127. package/dist/orchestrator/execution/agent-loop/bounded-agent-loop-runner.d.ts +1 -0
  128. package/dist/orchestrator/execution/agent-loop/bounded-agent-loop-runner.d.ts.map +1 -1
  129. package/dist/orchestrator/execution/agent-loop/bounded-agent-loop-runner.js +36 -2
  130. package/dist/orchestrator/execution/agent-loop/bounded-agent-loop-runner.js.map +1 -1
  131. package/dist/orchestrator/execution/agent-loop/chat-agent-loop-runner.d.ts.map +1 -1
  132. package/dist/orchestrator/execution/agent-loop/chat-agent-loop-runner.js +2 -1
  133. package/dist/orchestrator/execution/agent-loop/chat-agent-loop-runner.js.map +1 -1
  134. package/dist/orchestrator/execution/agent-loop/core-loop-control-tools.d.ts +19 -0
  135. package/dist/orchestrator/execution/agent-loop/core-loop-control-tools.d.ts.map +1 -1
  136. package/dist/orchestrator/execution/agent-loop/core-loop-control-tools.js +164 -14
  137. package/dist/orchestrator/execution/agent-loop/core-loop-control-tools.js.map +1 -1
  138. package/dist/orchestrator/execution/reflection-generator.d.ts +1 -0
  139. package/dist/orchestrator/execution/reflection-generator.d.ts.map +1 -1
  140. package/dist/orchestrator/execution/reflection-generator.js +10 -1
  141. package/dist/orchestrator/execution/reflection-generator.js.map +1 -1
  142. package/dist/orchestrator/execution/task/task-context-enricher.d.ts +2 -0
  143. package/dist/orchestrator/execution/task/task-context-enricher.d.ts.map +1 -1
  144. package/dist/orchestrator/execution/task/task-context-enricher.js +9 -4
  145. package/dist/orchestrator/execution/task/task-context-enricher.js.map +1 -1
  146. package/dist/orchestrator/execution/task/task-execution-helpers-internal.d.ts +5 -0
  147. package/dist/orchestrator/execution/task/task-execution-helpers-internal.d.ts.map +1 -0
  148. package/dist/orchestrator/execution/task/task-execution-helpers-internal.js +6 -0
  149. package/dist/orchestrator/execution/task/task-execution-helpers-internal.js.map +1 -0
  150. package/dist/orchestrator/execution/task/task-generation.d.ts.map +1 -1
  151. package/dist/orchestrator/execution/task/task-generation.js +8 -3
  152. package/dist/orchestrator/execution/task/task-generation.js.map +1 -1
  153. package/dist/orchestrator/execution/task/task-lifecycle-runner.d.ts +73 -0
  154. package/dist/orchestrator/execution/task/task-lifecycle-runner.d.ts.map +1 -0
  155. package/dist/orchestrator/execution/task/task-lifecycle-runner.js +184 -0
  156. package/dist/orchestrator/execution/task/task-lifecycle-runner.js.map +1 -0
  157. package/dist/orchestrator/execution/task/task-lifecycle.d.ts +7 -7
  158. package/dist/orchestrator/execution/task/task-lifecycle.d.ts.map +1 -1
  159. package/dist/orchestrator/execution/task/task-lifecycle.js +37 -181
  160. package/dist/orchestrator/execution/task/task-lifecycle.js.map +1 -1
  161. package/dist/orchestrator/execution/task/task-verifier-internal.d.ts +2 -0
  162. package/dist/orchestrator/execution/task/task-verifier-internal.d.ts.map +1 -0
  163. package/dist/orchestrator/execution/task/task-verifier-internal.js +2 -0
  164. package/dist/orchestrator/execution/task/task-verifier-internal.js.map +1 -0
  165. package/dist/orchestrator/goal/goal-negotiator.d.ts.map +1 -1
  166. package/dist/orchestrator/goal/goal-negotiator.js +23 -3
  167. package/dist/orchestrator/goal/goal-negotiator.js.map +1 -1
  168. package/dist/orchestrator/loop/core-loop/contracts.d.ts +1 -0
  169. package/dist/orchestrator/loop/core-loop/contracts.d.ts.map +1 -1
  170. package/dist/orchestrator/loop/core-loop/contracts.js.map +1 -1
  171. package/dist/orchestrator/loop/core-loop/iteration-kernel-knowledge.d.ts +7 -0
  172. package/dist/orchestrator/loop/core-loop/iteration-kernel-knowledge.d.ts.map +1 -0
  173. package/dist/orchestrator/loop/core-loop/iteration-kernel-knowledge.js +43 -0
  174. package/dist/orchestrator/loop/core-loop/iteration-kernel-knowledge.js.map +1 -0
  175. package/dist/orchestrator/loop/core-loop/iteration-kernel-wait.d.ts +14 -0
  176. package/dist/orchestrator/loop/core-loop/iteration-kernel-wait.d.ts.map +1 -0
  177. package/dist/orchestrator/loop/core-loop/iteration-kernel-wait.js +41 -0
  178. package/dist/orchestrator/loop/core-loop/iteration-kernel-wait.js.map +1 -0
  179. package/dist/orchestrator/loop/core-loop/iteration-kernel.d.ts +3 -2
  180. package/dist/orchestrator/loop/core-loop/iteration-kernel.d.ts.map +1 -1
  181. package/dist/orchestrator/loop/core-loop/iteration-kernel.js +16 -87
  182. package/dist/orchestrator/loop/core-loop/iteration-kernel.js.map +1 -1
  183. package/dist/orchestrator/loop/core-loop/phase-policy.js +5 -5
  184. package/dist/orchestrator/loop/core-loop/task-cycle-stall.d.ts +9 -0
  185. package/dist/orchestrator/loop/core-loop/task-cycle-stall.d.ts.map +1 -0
  186. package/dist/orchestrator/loop/core-loop/task-cycle-stall.js +297 -0
  187. package/dist/orchestrator/loop/core-loop/task-cycle-stall.js.map +1 -0
  188. package/dist/orchestrator/loop/core-loop/task-cycle-wait.d.ts +11 -0
  189. package/dist/orchestrator/loop/core-loop/task-cycle-wait.d.ts.map +1 -0
  190. package/dist/orchestrator/loop/core-loop/task-cycle-wait.js +176 -0
  191. package/dist/orchestrator/loop/core-loop/task-cycle-wait.js.map +1 -0
  192. package/dist/orchestrator/loop/core-loop/task-cycle.d.ts +3 -15
  193. package/dist/orchestrator/loop/core-loop/task-cycle.d.ts.map +1 -1
  194. package/dist/orchestrator/loop/core-loop/task-cycle.js +10 -444
  195. package/dist/orchestrator/loop/core-loop/task-cycle.js.map +1 -1
  196. package/dist/orchestrator/loop/core-loop.d.ts +3 -0
  197. package/dist/orchestrator/loop/core-loop.d.ts.map +1 -1
  198. package/dist/orchestrator/loop/core-loop.js +4 -0
  199. package/dist/orchestrator/loop/core-loop.js.map +1 -1
  200. package/dist/orchestrator/strategy/portfolio-manager.d.ts +3 -2
  201. package/dist/orchestrator/strategy/portfolio-manager.d.ts.map +1 -1
  202. package/dist/orchestrator/strategy/portfolio-manager.js +16 -11
  203. package/dist/orchestrator/strategy/portfolio-manager.js.map +1 -1
  204. package/dist/orchestrator/strategy/portfolio-rebalance.d.ts +1 -1
  205. package/dist/orchestrator/strategy/portfolio-rebalance.d.ts.map +1 -1
  206. package/dist/orchestrator/strategy/portfolio-rebalance.js +31 -9
  207. package/dist/orchestrator/strategy/portfolio-rebalance.js.map +1 -1
  208. package/dist/orchestrator/strategy/strategy-manager-base.d.ts +12 -2
  209. package/dist/orchestrator/strategy/strategy-manager-base.d.ts.map +1 -1
  210. package/dist/orchestrator/strategy/strategy-manager-base.js +23 -7
  211. package/dist/orchestrator/strategy/strategy-manager-base.js.map +1 -1
  212. package/dist/orchestrator/strategy/strategy-manager.d.ts +1 -11
  213. package/dist/orchestrator/strategy/strategy-manager.d.ts.map +1 -1
  214. package/dist/orchestrator/strategy/strategy-manager.js +78 -19
  215. package/dist/orchestrator/strategy/strategy-manager.js.map +1 -1
  216. package/dist/platform/dream/dream-consolidator/fs-metrics.d.ts +18 -0
  217. package/dist/platform/dream/dream-consolidator/fs-metrics.d.ts.map +1 -0
  218. package/dist/platform/dream/dream-consolidator/fs-metrics.js +130 -0
  219. package/dist/platform/dream/dream-consolidator/fs-metrics.js.map +1 -0
  220. package/dist/platform/dream/dream-consolidator.d.ts +4 -14
  221. package/dist/platform/dream/dream-consolidator.d.ts.map +1 -1
  222. package/dist/platform/dream/dream-consolidator.js +46 -166
  223. package/dist/platform/dream/dream-consolidator.js.map +1 -1
  224. package/dist/platform/dream/dream-soil-sync.d.ts +1 -0
  225. package/dist/platform/dream/dream-soil-sync.d.ts.map +1 -1
  226. package/dist/platform/dream/dream-soil-sync.js +8 -1
  227. package/dist/platform/dream/dream-soil-sync.js.map +1 -1
  228. package/dist/platform/dream/dream-types.d.ts +5 -0
  229. package/dist/platform/dream/dream-types.d.ts.map +1 -1
  230. package/dist/platform/dream/dream-types.js +1 -0
  231. package/dist/platform/dream/dream-types.js.map +1 -1
  232. package/dist/platform/dream/playbook-memory.d.ts +4 -4
  233. package/dist/platform/drive/stall-detector/analysis.d.ts +5 -0
  234. package/dist/platform/drive/stall-detector/analysis.d.ts.map +1 -0
  235. package/dist/platform/drive/stall-detector/analysis.js +55 -0
  236. package/dist/platform/drive/stall-detector/analysis.js.map +1 -0
  237. package/dist/platform/drive/stall-detector/repetitive.d.ts +3 -0
  238. package/dist/platform/drive/stall-detector/repetitive.d.ts.map +1 -0
  239. package/dist/platform/drive/stall-detector/repetitive.js +72 -0
  240. package/dist/platform/drive/stall-detector/repetitive.js.map +1 -0
  241. package/dist/platform/drive/stall-detector/thresholds.d.ts +10 -0
  242. package/dist/platform/drive/stall-detector/thresholds.d.ts.map +1 -0
  243. package/dist/platform/drive/stall-detector/thresholds.js +61 -0
  244. package/dist/platform/drive/stall-detector/thresholds.js.map +1 -0
  245. package/dist/platform/drive/stall-detector.d.ts +2 -20
  246. package/dist/platform/drive/stall-detector.d.ts.map +1 -1
  247. package/dist/platform/drive/stall-detector.js +9 -202
  248. package/dist/platform/drive/stall-detector.js.map +1 -1
  249. package/dist/platform/knowledge/knowledge-manager-agent-memory.d.ts +55 -0
  250. package/dist/platform/knowledge/knowledge-manager-agent-memory.d.ts.map +1 -0
  251. package/dist/platform/knowledge/knowledge-manager-agent-memory.js +232 -0
  252. package/dist/platform/knowledge/knowledge-manager-agent-memory.js.map +1 -0
  253. package/dist/platform/knowledge/knowledge-manager-internals.d.ts +10 -0
  254. package/dist/platform/knowledge/knowledge-manager-internals.d.ts.map +1 -0
  255. package/dist/platform/knowledge/knowledge-manager-internals.js +43 -0
  256. package/dist/platform/knowledge/knowledge-manager-internals.js.map +1 -0
  257. package/dist/platform/knowledge/knowledge-manager-store.d.ts +13 -0
  258. package/dist/platform/knowledge/knowledge-manager-store.d.ts.map +1 -0
  259. package/dist/platform/knowledge/knowledge-manager-store.js +67 -0
  260. package/dist/platform/knowledge/knowledge-manager-store.js.map +1 -0
  261. package/dist/platform/knowledge/knowledge-manager.d.ts +6 -2
  262. package/dist/platform/knowledge/knowledge-manager.d.ts.map +1 -1
  263. package/dist/platform/knowledge/knowledge-manager.js +43 -344
  264. package/dist/platform/knowledge/knowledge-manager.js.map +1 -1
  265. package/dist/platform/knowledge/memory/memory-lifecycle-storage.d.ts +4 -0
  266. package/dist/platform/knowledge/memory/memory-lifecycle-storage.d.ts.map +1 -0
  267. package/dist/platform/knowledge/memory/memory-lifecycle-storage.js +106 -0
  268. package/dist/platform/knowledge/memory/memory-lifecycle-storage.js.map +1 -0
  269. package/dist/platform/knowledge/memory/memory-lifecycle.d.ts.map +1 -1
  270. package/dist/platform/knowledge/memory/memory-lifecycle.js +6 -112
  271. package/dist/platform/knowledge/memory/memory-lifecycle.js.map +1 -1
  272. package/dist/platform/observation/capability-detector/prompts.d.ts +18 -0
  273. package/dist/platform/observation/capability-detector/prompts.d.ts.map +1 -0
  274. package/dist/platform/observation/capability-detector/prompts.js +80 -0
  275. package/dist/platform/observation/capability-detector/prompts.js.map +1 -0
  276. package/dist/platform/observation/capability-detector/recommendations.d.ts +5 -0
  277. package/dist/platform/observation/capability-detector/recommendations.d.ts.map +1 -0
  278. package/dist/platform/observation/capability-detector/recommendations.js +76 -0
  279. package/dist/platform/observation/capability-detector/recommendations.js.map +1 -0
  280. package/dist/platform/observation/capability-detector/types.d.ts +112 -0
  281. package/dist/platform/observation/capability-detector/types.d.ts.map +1 -0
  282. package/dist/platform/observation/capability-detector/types.js +75 -0
  283. package/dist/platform/observation/capability-detector/types.js.map +1 -0
  284. package/dist/platform/observation/capability-detector.d.ts +4 -9
  285. package/dist/platform/observation/capability-detector.d.ts.map +1 -1
  286. package/dist/platform/observation/capability-detector.js +12 -212
  287. package/dist/platform/observation/capability-detector.js.map +1 -1
  288. package/dist/platform/observation/context-provider/collector.d.ts +13 -0
  289. package/dist/platform/observation/context-provider/collector.d.ts.map +1 -0
  290. package/dist/platform/observation/context-provider/collector.js +259 -0
  291. package/dist/platform/observation/context-provider/collector.js.map +1 -0
  292. package/dist/platform/observation/context-provider/search-terms.d.ts +2 -0
  293. package/dist/platform/observation/context-provider/search-terms.d.ts.map +1 -0
  294. package/dist/platform/observation/context-provider/search-terms.js +24 -0
  295. package/dist/platform/observation/context-provider/search-terms.js.map +1 -0
  296. package/dist/platform/observation/context-provider/shared.d.ts +17 -0
  297. package/dist/platform/observation/context-provider/shared.d.ts.map +1 -0
  298. package/dist/platform/observation/context-provider/shared.js +87 -0
  299. package/dist/platform/observation/context-provider/shared.js.map +1 -0
  300. package/dist/platform/observation/context-provider.d.ts +3 -28
  301. package/dist/platform/observation/context-provider.d.ts.map +1 -1
  302. package/dist/platform/observation/context-provider.js +7 -395
  303. package/dist/platform/observation/context-provider.js.map +1 -1
  304. package/dist/platform/soil/compiled-memory-projections.d.ts +2 -0
  305. package/dist/platform/soil/compiled-memory-projections.d.ts.map +1 -1
  306. package/dist/platform/soil/compiled-memory-projections.js +59 -0
  307. package/dist/platform/soil/compiled-memory-projections.js.map +1 -1
  308. package/dist/platform/soil/contracts.d.ts +2 -2
  309. package/dist/platform/soil/retriever.d.ts +25 -0
  310. package/dist/platform/soil/retriever.d.ts.map +1 -1
  311. package/dist/platform/soil/retriever.js +94 -5
  312. package/dist/platform/soil/retriever.js.map +1 -1
  313. package/dist/platform/soil/sqlite-repository-helpers.d.ts +80 -0
  314. package/dist/platform/soil/sqlite-repository-helpers.d.ts.map +1 -0
  315. package/dist/platform/soil/sqlite-repository-helpers.js +143 -0
  316. package/dist/platform/soil/sqlite-repository-helpers.js.map +1 -0
  317. package/dist/platform/soil/sqlite-repository-search.d.ts +8 -0
  318. package/dist/platform/soil/sqlite-repository-search.d.ts.map +1 -0
  319. package/dist/platform/soil/sqlite-repository-search.js +367 -0
  320. package/dist/platform/soil/sqlite-repository-search.js.map +1 -0
  321. package/dist/platform/soil/sqlite-repository-storage.d.ts +8 -0
  322. package/dist/platform/soil/sqlite-repository-storage.d.ts.map +1 -0
  323. package/dist/platform/soil/sqlite-repository-storage.js +278 -0
  324. package/dist/platform/soil/sqlite-repository-storage.js.map +1 -0
  325. package/dist/platform/soil/sqlite-repository.d.ts +1 -4
  326. package/dist/platform/soil/sqlite-repository.d.ts.map +1 -1
  327. package/dist/platform/soil/sqlite-repository.js +26 -820
  328. package/dist/platform/soil/sqlite-repository.js.map +1 -1
  329. package/dist/runtime/daemon/index.d.ts +1 -1
  330. package/dist/runtime/daemon/index.d.ts.map +1 -1
  331. package/dist/runtime/daemon/index.js +1 -1
  332. package/dist/runtime/daemon/index.js.map +1 -1
  333. package/dist/runtime/daemon/maintenance.d.ts +2 -10
  334. package/dist/runtime/daemon/maintenance.d.ts.map +1 -1
  335. package/dist/runtime/daemon/maintenance.js +14 -45
  336. package/dist/runtime/daemon/maintenance.js.map +1 -1
  337. package/dist/runtime/daemon/runner-bootstrap.d.ts +25 -0
  338. package/dist/runtime/daemon/runner-bootstrap.d.ts.map +1 -0
  339. package/dist/runtime/daemon/runner-bootstrap.js +77 -0
  340. package/dist/runtime/daemon/runner-bootstrap.js.map +1 -0
  341. package/dist/runtime/daemon/runner-commands.d.ts +7 -7
  342. package/dist/runtime/daemon/runner-commands.d.ts.map +1 -1
  343. package/dist/runtime/daemon/runner-commands.js +35 -20
  344. package/dist/runtime/daemon/runner-commands.js.map +1 -1
  345. package/dist/runtime/daemon/runner-goal-cycle.d.ts.map +1 -1
  346. package/dist/runtime/daemon/runner-goal-cycle.js +3 -5
  347. package/dist/runtime/daemon/runner-goal-cycle.js.map +1 -1
  348. package/dist/runtime/daemon/runner-resident-curiosity.d.ts +12 -0
  349. package/dist/runtime/daemon/runner-resident-curiosity.d.ts.map +1 -0
  350. package/dist/runtime/daemon/runner-resident-curiosity.js +155 -0
  351. package/dist/runtime/daemon/runner-resident-curiosity.js.map +1 -0
  352. package/dist/runtime/daemon/runner-resident-dream.d.ts +20 -0
  353. package/dist/runtime/daemon/runner-resident-dream.d.ts.map +1 -0
  354. package/dist/runtime/daemon/runner-resident-dream.js +148 -0
  355. package/dist/runtime/daemon/runner-resident-dream.js.map +1 -0
  356. package/dist/runtime/daemon/runner-resident-proactive.d.ts +4 -0
  357. package/dist/runtime/daemon/runner-resident-proactive.d.ts.map +1 -0
  358. package/dist/runtime/daemon/runner-resident-proactive.js +113 -0
  359. package/dist/runtime/daemon/runner-resident-proactive.js.map +1 -0
  360. package/dist/runtime/daemon/runner-resident-shared.d.ts +40 -0
  361. package/dist/runtime/daemon/runner-resident-shared.d.ts.map +1 -0
  362. package/dist/runtime/daemon/runner-resident-shared.js +101 -0
  363. package/dist/runtime/daemon/runner-resident-shared.js.map +1 -0
  364. package/dist/runtime/daemon/runner-resident.d.ts +4 -68
  365. package/dist/runtime/daemon/runner-resident.d.ts.map +1 -1
  366. package/dist/runtime/daemon/runner-resident.js +4 -506
  367. package/dist/runtime/daemon/runner-resident.js.map +1 -1
  368. package/dist/runtime/daemon/runner-runtime.d.ts +12 -0
  369. package/dist/runtime/daemon/runner-runtime.d.ts.map +1 -0
  370. package/dist/runtime/daemon/runner-runtime.js +43 -0
  371. package/dist/runtime/daemon/runner-runtime.js.map +1 -0
  372. package/dist/runtime/daemon/runner-startup.js +3 -4
  373. package/dist/runtime/daemon/runner-startup.js.map +1 -1
  374. package/dist/runtime/daemon/runner.d.ts +10 -23
  375. package/dist/runtime/daemon/runner.d.ts.map +1 -1
  376. package/dist/runtime/daemon/runner.js +27 -110
  377. package/dist/runtime/daemon/runner.js.map +1 -1
  378. package/dist/runtime/daemon/wait-deadline-resolver.d.ts +5 -2
  379. package/dist/runtime/daemon/wait-deadline-resolver.d.ts.map +1 -1
  380. package/dist/runtime/daemon/wait-deadline-resolver.js +55 -35
  381. package/dist/runtime/daemon/wait-deadline-resolver.js.map +1 -1
  382. package/dist/runtime/event/dispatcher.d.ts +0 -2
  383. package/dist/runtime/event/dispatcher.d.ts.map +1 -1
  384. package/dist/runtime/event/dispatcher.js +0 -4
  385. package/dist/runtime/event/dispatcher.js.map +1 -1
  386. package/dist/runtime/executor/goal-worker.d.ts +2 -1
  387. package/dist/runtime/executor/goal-worker.d.ts.map +1 -1
  388. package/dist/runtime/executor/goal-worker.js +2 -1
  389. package/dist/runtime/executor/goal-worker.js.map +1 -1
  390. package/dist/runtime/executor/loop-supervisor.d.ts +3 -0
  391. package/dist/runtime/executor/loop-supervisor.d.ts.map +1 -1
  392. package/dist/runtime/executor/loop-supervisor.js +37 -11
  393. package/dist/runtime/executor/loop-supervisor.js.map +1 -1
  394. package/dist/runtime/schedule/engine-cron-reflection.d.ts +31 -0
  395. package/dist/runtime/schedule/engine-cron-reflection.d.ts.map +1 -0
  396. package/dist/runtime/schedule/engine-cron-reflection.js +229 -0
  397. package/dist/runtime/schedule/engine-cron-reflection.js.map +1 -0
  398. package/dist/runtime/schedule/engine-execution.d.ts +47 -0
  399. package/dist/runtime/schedule/engine-execution.d.ts.map +1 -0
  400. package/dist/runtime/schedule/engine-execution.js +424 -0
  401. package/dist/runtime/schedule/engine-execution.js.map +1 -0
  402. package/dist/runtime/schedule/engine-heartbeat.d.ts +5 -0
  403. package/dist/runtime/schedule/engine-heartbeat.d.ts.map +1 -0
  404. package/dist/runtime/schedule/engine-heartbeat.js +104 -0
  405. package/dist/runtime/schedule/engine-heartbeat.js.map +1 -0
  406. package/dist/runtime/schedule/engine-layers.d.ts +2 -0
  407. package/dist/runtime/schedule/engine-layers.d.ts.map +1 -1
  408. package/dist/runtime/schedule/engine-layers.js +12 -228
  409. package/dist/runtime/schedule/engine-layers.js.map +1 -1
  410. package/dist/runtime/schedule/engine-mutations.d.ts +37 -0
  411. package/dist/runtime/schedule/engine-mutations.d.ts.map +1 -0
  412. package/dist/runtime/schedule/engine-mutations.js +263 -0
  413. package/dist/runtime/schedule/engine-mutations.js.map +1 -0
  414. package/dist/runtime/schedule/engine.d.ts +11 -38
  415. package/dist/runtime/schedule/engine.d.ts.map +1 -1
  416. package/dist/runtime/schedule/engine.js +65 -810
  417. package/dist/runtime/schedule/engine.js.map +1 -1
  418. package/dist/runtime/schedule/history.d.ts +16 -0
  419. package/dist/runtime/schedule/history.d.ts.map +1 -1
  420. package/dist/runtime/schedule/history.js +8 -0
  421. package/dist/runtime/schedule/history.js.map +1 -1
  422. package/dist/runtime/schedule/index.d.ts +1 -0
  423. package/dist/runtime/schedule/index.d.ts.map +1 -1
  424. package/dist/runtime/schedule/index.js +1 -0
  425. package/dist/runtime/schedule/index.js.map +1 -1
  426. package/dist/runtime/schedule/legacy-cron-migration.d.ts +9 -0
  427. package/dist/runtime/schedule/legacy-cron-migration.d.ts.map +1 -0
  428. package/dist/runtime/schedule/legacy-cron-migration.js +89 -0
  429. package/dist/runtime/schedule/legacy-cron-migration.js.map +1 -0
  430. package/dist/runtime/schedule/wait-projection.d.ts +6 -0
  431. package/dist/runtime/schedule/wait-projection.d.ts.map +1 -0
  432. package/dist/runtime/schedule/wait-projection.js +102 -0
  433. package/dist/runtime/schedule/wait-projection.js.map +1 -0
  434. package/dist/runtime/session-registry/registry-helpers.d.ts +34 -0
  435. package/dist/runtime/session-registry/registry-helpers.d.ts.map +1 -0
  436. package/dist/runtime/session-registry/registry-helpers.js +241 -0
  437. package/dist/runtime/session-registry/registry-helpers.js.map +1 -0
  438. package/dist/runtime/session-registry/registry.d.ts.map +1 -1
  439. package/dist/runtime/session-registry/registry.js +14 -243
  440. package/dist/runtime/session-registry/registry.js.map +1 -1
  441. package/dist/runtime/session-registry/types.d.ts +36 -36
  442. package/dist/runtime/store/runtime-operation-schemas.d.ts +36 -36
  443. package/dist/runtime/store/runtime-schemas.d.ts +2 -2
  444. package/dist/runtime/types/daemon.d.ts +10 -0
  445. package/dist/runtime/types/daemon.d.ts.map +1 -1
  446. package/dist/runtime/types/daemon.js +2 -0
  447. package/dist/runtime/types/daemon.js.map +1 -1
  448. package/dist/runtime/types/schedule.d.ts +65 -0
  449. package/dist/runtime/types/schedule.d.ts.map +1 -1
  450. package/dist/runtime/types/schedule.js +5 -0
  451. package/dist/runtime/types/schedule.js.map +1 -1
  452. package/dist/tools/builtin/exports.d.ts +1 -0
  453. package/dist/tools/builtin/exports.d.ts.map +1 -1
  454. package/dist/tools/builtin/exports.js +1 -0
  455. package/dist/tools/builtin/exports.js.map +1 -1
  456. package/dist/tools/builtin/factory.d.ts +2 -0
  457. package/dist/tools/builtin/factory.d.ts.map +1 -1
  458. package/dist/tools/builtin/factory.js +8 -1
  459. package/dist/tools/builtin/factory.js.map +1 -1
  460. package/dist/tools/fs/FileValidationTool/protected-path-policy.d.ts +1 -0
  461. package/dist/tools/fs/FileValidationTool/protected-path-policy.d.ts.map +1 -1
  462. package/dist/tools/fs/FileValidationTool/protected-path-policy.js +17 -4
  463. package/dist/tools/fs/FileValidationTool/protected-path-policy.js.map +1 -1
  464. package/dist/tools/fs/ListDirTool/ListDirTool.js +1 -1
  465. package/dist/tools/fs/ListDirTool/ListDirTool.js.map +1 -1
  466. package/dist/tools/kaggle/paths.d.ts.map +1 -1
  467. package/dist/tools/kaggle/paths.js +6 -0
  468. package/dist/tools/kaggle/paths.js.map +1 -1
  469. package/dist/tools/query/CodeSearchRepairTool/CodeSearchRepairTool.d.ts.map +1 -1
  470. package/dist/tools/query/CodeSearchRepairTool/CodeSearchRepairTool.js +23 -2
  471. package/dist/tools/query/CodeSearchRepairTool/CodeSearchRepairTool.js.map +1 -1
  472. package/dist/tools/query/CodeSearchTool/CodeSearchTool.d.ts.map +1 -1
  473. package/dist/tools/query/CodeSearchTool/CodeSearchTool.js +24 -2
  474. package/dist/tools/query/CodeSearchTool/CodeSearchTool.js.map +1 -1
  475. package/dist/tools/query/SoilQueryTool/SoilQueryTool.d.ts.map +1 -1
  476. package/dist/tools/query/SoilQueryTool/SoilQueryTool.js +43 -9
  477. package/dist/tools/query/SoilQueryTool/SoilQueryTool.js.map +1 -1
  478. package/dist/tools/query/code-search-root.d.ts +8 -0
  479. package/dist/tools/query/code-search-root.d.ts.map +1 -0
  480. package/dist/tools/query/code-search-root.js +41 -0
  481. package/dist/tools/query/code-search-root.js.map +1 -0
  482. package/dist/tools/query/runtime-session-tools.d.ts +560 -0
  483. package/dist/tools/query/runtime-session-tools.d.ts.map +1 -0
  484. package/dist/tools/query/runtime-session-tools.js +1015 -0
  485. package/dist/tools/query/runtime-session-tools.js.map +1 -0
  486. package/dist/tools/runtime/LongRunningRuntimeTools.d.ts +821 -0
  487. package/dist/tools/runtime/LongRunningRuntimeTools.d.ts.map +1 -0
  488. package/dist/tools/runtime/LongRunningRuntimeTools.js +845 -0
  489. package/dist/tools/runtime/LongRunningRuntimeTools.js.map +1 -0
  490. package/dist/tools/system/ProcessSessionTool/ProcessSessionTool.d.ts +1 -0
  491. package/dist/tools/system/ProcessSessionTool/ProcessSessionTool.d.ts.map +1 -1
  492. package/dist/tools/system/ProcessSessionTool/ProcessSessionTool.js +11 -0
  493. package/dist/tools/system/ProcessSessionTool/ProcessSessionTool.js.map +1 -1
  494. package/dist/tools/system/ShellTool/ShellTool.d.ts.map +1 -1
  495. package/dist/tools/system/ShellTool/ShellTool.js +2 -1
  496. package/dist/tools/system/ShellTool/ShellTool.js.map +1 -1
  497. package/dist/tools/types.d.ts +2 -0
  498. package/dist/tools/types.d.ts.map +1 -1
  499. package/dist/tools/types.js.map +1 -1
  500. package/package.json +1 -1
  501. package/dist/base/types/cron.d.ts +0 -2
  502. package/dist/base/types/cron.d.ts.map +0 -1
  503. package/dist/base/types/cron.js +0 -3
  504. package/dist/base/types/cron.js.map +0 -1
  505. package/dist/runtime/cron-scheduler.d.ts +0 -13
  506. package/dist/runtime/cron-scheduler.d.ts.map +0 -1
  507. package/dist/runtime/cron-scheduler.js +0 -90
  508. package/dist/runtime/cron-scheduler.js.map +0 -1
  509. package/dist/runtime/types/cron.d.ts +0 -59
  510. package/dist/runtime/types/cron.d.ts.map +0 -1
  511. package/dist/runtime/types/cron.js +0 -13
  512. package/dist/runtime/types/cron.js.map +0 -1
@@ -2,180 +2,36 @@
2
2
  //
3
3
  // Central coordinator for 1-shot chat execution (Tier 1).
4
4
  // Bypasses TaskLifecycle — calls adapter.execute() directly.
5
- import { execFile } from "node:child_process";
6
- import * as fsp from "node:fs/promises";
7
- import * as path from "node:path";
8
5
  import { getPulseedDirPath } from "../../base/utils/paths.js";
9
6
  import { getSelfIdentityResponseForBaseDir } from "../../base/config/identity-loader.js";
10
- import { loadProviderConfig } from "../../base/llm/provider-config.js";
11
- import { TaskSchema } from "../../base/types/task.js";
12
7
  import { ChatHistory } from "./chat-history.js";
13
8
  import { ChatSessionCatalog, ChatSessionSelectorError, } from "./chat-session-store.js";
14
9
  import { buildChatContext, resolveGitRoot } from "../../platform/observation/context-provider.js";
15
10
  import { buildChatAgentLoopSystemPrompt, buildStaticSystemPrompt, createChatGroundingGateway } from "./grounding.js";
16
- import { verifyChatAction } from "./chat-verifier.js";
17
- import { toToolDefinitionsFiltered } from "../../tools/tool-definition-adapter.js";
18
- import { TendCommand } from "./tend-command.js";
19
- import { EventSubscriber } from "./event-subscriber.js";
20
- import { buildPromptedToolProtocolSystemPrompt, extractPromptedToolCalls, } from "../../orchestrator/execution/agent-loop/prompted-tool-protocol.js";
21
- import { classifyFailureRecovery, formatFailureRecovery, formatLifecycleFailureMessage } from "./failure-recovery.js";
22
- import { resolveExecutionPolicy, summarizeExecutionPolicy, withExecutionPolicyOverrides, } from "../../orchestrator/execution/agent-loop/execution-policy.js";
23
- import { buildStandaloneIngressMessage, createIngressRouter, } from "./ingress-router.js";
24
- import { createRuntimeSessionRegistry } from "../../runtime/session-registry/index.js";
11
+ import { createIngressRouter, } from "./ingress-router.js";
12
+ import { classifyInterruptRedirect, collectGitDiffArtifact, previewActivityText } from "./chat-runner-support.js";
13
+ import { COMMAND_HELP, ChatRunnerCommandHandler, } from "./chat-runner-commands.js";
14
+ import { ChatRunnerEventBridge } from "./chat-runner-event-bridge.js";
15
+ import { buildRuntimeControlContextFromIngress, buildStandaloneIngressMessageFromContext, formatRoute, getRouteCapabilities, loadedSessionToChatSession, resolveChatResumeSelector, } from "./chat-runner-runtime.js";
16
+ import { executeAdapterRoute, executeAgentLoopRoute, executeRuntimeControlRoute, executeToolLoopRoute, resolveSessionExecutionPolicy, } from "./chat-runner-routes.js";
25
17
  const DEFAULT_TIMEOUT_MS = 120_000;
26
- const MAX_VERIFY_RETRIES = 2;
27
- const MAX_TOOL_LOOPS = 5;
28
- const ACTIVITY_PREVIEW_CHARS = 40;
29
- const DIFF_ARTIFACT_MAX_LINES = 80;
30
- const standaloneIngressRouter = createIngressRouter();
31
- // ─── Command help text ───
32
- const COMMAND_HELP = `Available commands:
33
- Session
34
- /help Show this help message
35
- /clear Clear conversation history
36
- /sessions List prior chat sessions
37
- /history [id|title] Show saved chat history
38
- /title <title> Rename the current session
39
- /resume [id|title] Resume native agentloop state for the current or selected session
40
- /cleanup [--dry-run] Clean up stale chat sessions
41
- /compact Summarize older chat turns and keep the latest turns
42
- /context Show active working context and session assumptions
43
- /exit Exit chat mode
44
-
45
- Goals and tasks
46
- /status [goal-id] Show active goal status, or one goal when an id is provided
47
- /goals List goals
48
- /tasks [goal-id] List tasks for a goal; uses the only active goal when unambiguous
49
- /task <task-id> [goal-id]
50
- Show one task; searches goals when no goal id is provided
51
- /track Promote session to Tier 2 goal pursuit (not yet implemented)
52
- /tend Generate a goal from chat history and start autonomous daemon execution
53
-
54
- Configuration
55
- /config Show provider configuration with secrets masked
56
- /model Show the active provider/model/adapter
57
- /permissions [args] Show or update session execution policy
58
- /plugins List installed plugins when plugin metadata is available
59
- /usage [scope] Show usage summary (session, goal <id>, daemon <goal-id>, schedule [7d|24h|2w])
60
-
61
- Review and branching
62
- /review Show current diff summary and verification context
63
- /fork [title] Fork the current chat session into a new session
64
- /undo Remove the latest chat turn from session history
65
-
66
- Deferred
67
- /retry is intentionally not supported yet.`;
68
- // ─── Helpers ───
69
- function checkGitChanges(cwd) {
70
- return new Promise((resolve) => {
71
- execFile("git", ["diff", "HEAD", "--stat"], { cwd, timeout: 5_000 }, (err, stdout, stderr) => {
72
- resolve(err ? null : (stdout + stderr).trim());
73
- });
74
- });
75
- }
76
- function runGit(cwd, args, timeout = 5_000) {
77
- return new Promise((resolve) => {
78
- execFile("git", args, { cwd, timeout }, (err, stdout, stderr) => {
79
- if (err) {
80
- resolve(null);
81
- return;
82
- }
83
- resolve((stdout + stderr).trim());
84
- });
85
- });
86
- }
87
- function parseGitLines(output) {
88
- return output ? output.split("\n").map((line) => line.trim()).filter(Boolean) : [];
89
- }
90
- async function buildUntrackedFilePatch(cwd, relativePath) {
91
- const absolutePath = path.resolve(cwd, relativePath);
92
- const relativeFromCwd = path.relative(cwd, absolutePath);
93
- if (relativeFromCwd.startsWith("..") || path.isAbsolute(relativeFromCwd)) {
94
- return `diff --git a/${relativePath} b/${relativePath}\nnew file skipped: path outside workspace`;
95
- }
96
- try {
97
- const stat = await fsp.stat(absolutePath);
98
- if (!stat.isFile()) {
99
- return `diff --git a/${relativePath} b/${relativePath}\nnew file skipped: not a regular file`;
100
- }
101
- if (stat.size > 100_000) {
102
- return `diff --git a/${relativePath} b/${relativePath}\nnew file skipped: ${stat.size} bytes`;
103
- }
104
- const content = await fsp.readFile(absolutePath, "utf-8");
105
- const lines = content.split("\n");
106
- const body = lines.map((line) => `+${line}`).join("\n");
107
- return [
108
- `diff --git a/${relativePath} b/${relativePath}`,
109
- "new file mode 100644",
110
- "--- /dev/null",
111
- `+++ b/${relativePath}`,
112
- `@@ -0,0 +1,${lines.length} @@`,
113
- body,
114
- ].join("\n");
115
- }
116
- catch {
117
- return `diff --git a/${relativePath} b/${relativePath}\nnew file skipped: unreadable`;
118
- }
119
- }
120
- async function collectGitDiffArtifact(cwd) {
121
- const trackedStat = await runGit(cwd, ["diff", "HEAD", "--stat"]);
122
- const untrackedFiles = parseGitLines(await runGit(cwd, ["ls-files", "--others", "--exclude-standard"]));
123
- if (!trackedStat && untrackedFiles.length === 0)
18
+ function normalizePinnedReplyTarget(replyTarget) {
19
+ if (!replyTarget)
20
+ return null;
21
+ const channel = replyTarget.channel ?? replyTarget.surface;
22
+ if (!channel)
124
23
  return null;
125
- const trackedNameStatus = await runGit(cwd, ["diff", "HEAD", "--name-status"]) ?? "";
126
- const trackedPatch = await runGit(cwd, ["diff", "HEAD", "--patch", "--unified=3"], 10_000) ?? "";
127
- const untrackedPatchParts = await Promise.all(untrackedFiles.slice(0, 10).map((file) => buildUntrackedFilePatch(cwd, file)));
128
- if (untrackedFiles.length > 10) {
129
- untrackedPatchParts.push(`... ${untrackedFiles.length - 10} additional untracked file(s) omitted`);
130
- }
131
- const stat = [
132
- trackedStat,
133
- untrackedFiles.length > 0
134
- ? ["Untracked files:", ...untrackedFiles.map((file) => ` ${file}`)].join("\n")
135
- : "",
136
- ].filter(Boolean).join("\n");
137
- const nameStatus = [
138
- trackedNameStatus,
139
- ...untrackedFiles.map((file) => `A\t${file}`),
140
- ].filter(Boolean).join("\n");
141
- const patch = [trackedPatch, ...untrackedPatchParts].filter(Boolean).join("\n");
142
- const patchLines = patch.split("\n");
143
- const truncated = patchLines.length > DIFF_ARTIFACT_MAX_LINES;
144
24
  return {
145
- stat,
146
- nameStatus,
147
- patch: patchLines.slice(0, DIFF_ARTIFACT_MAX_LINES).join("\n"),
148
- truncated,
25
+ channel,
26
+ target_id: replyTarget.conversation_id ?? replyTarget.identity_key ?? replyTarget.response_channel ?? null,
27
+ thread_id: replyTarget.message_id ?? null,
28
+ metadata: {
29
+ ...replyTarget,
30
+ ...(replyTarget.metadata ?? {}),
31
+ },
149
32
  };
150
33
  }
151
- function previewActivityText(value, maxChars = ACTIVITY_PREVIEW_CHARS) {
152
- const normalized = value.replace(/\s+/g, " ").trim();
153
- return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}...` : normalized;
154
- }
155
- function classifyInterruptRedirect(input) {
156
- const normalized = input.trim().toLowerCase();
157
- if (/\b(background|bg)\b|バックグラウンド|裏で|裏側|continue.*background/.test(normalized)) {
158
- return "background";
159
- }
160
- if (/\b(review|read.?only|readonly)\b|レビュー|確認だけ|読むだけ/.test(normalized)) {
161
- return "review";
162
- }
163
- if (/\b(diff|changes?|patch)\b|差分|変更.*見|変更内容/.test(normalized)) {
164
- return "diff";
165
- }
166
- if (/\b(stop|pause|summary|summarize|interrupt)\b|止め|停止|中断|一旦|要約/.test(normalized)) {
167
- return "summary";
168
- }
169
- return "redirect";
170
- }
171
- function formatToolActivity(action, toolName, detail) {
172
- const preview = detail ? previewActivityText(detail) : "";
173
- return preview ? `${action} tool: ${toolName} - ${preview}` : `${action} tool: ${toolName}`;
174
- }
175
- function formatIntentInput(input, maxChars = 96) {
176
- const normalized = input.replace(/\s+/g, " ").trim();
177
- return normalized.length > maxChars ? `${normalized.slice(0, maxChars - 3)}...` : normalized;
178
- }
34
+ const standaloneIngressRouter = createIngressRouter();
179
35
  function resolveSelfIdentityResponse(input, baseDir) {
180
36
  const normalized = input.trim().toLowerCase().replace(/\s+/g, "");
181
37
  if (!normalized)
@@ -190,45 +46,52 @@ function resolveSelfIdentityResponse(input, baseDir) {
190
46
  return null;
191
47
  return getSelfIdentityResponseForBaseDir(baseDir, isEnglishIdentityQuestion ? "en" : "ja");
192
48
  }
193
- // ─── ChatRunner ───
194
49
  export class ChatRunner {
195
50
  deps;
196
51
  groundingGateway;
52
+ eventBridge;
53
+ commandHandler;
197
54
  history = null;
198
55
  sessionCwd = null;
199
- /** True when startSession() has been called — enables session persistence across execute() calls. */
200
56
  sessionActive = false;
201
- /** Deferred tools activated by ToolSearch results — included in tool definitions for subsequent turns. */
202
57
  activatedTools = new Set();
203
- /** Cached static system prompt — reused across turns; dynamic context is rebuilt each turn. */
204
58
  cachedStaticSystemPrompt = null;
205
- /** Pending /tend state awaiting user confirmation (Y/n). */
206
59
  pendingTend = null;
207
- /** Active EventSubscriber instances keyed by goalId. */
208
60
  activeSubscribers = new Map();
209
- /**
210
- * Callback invoked when a /tend daemon notification arrives.
211
- * Can be set after construction (e.g. from a React component via useEffect).
212
- */
213
61
  onNotification = undefined;
214
62
  onEvent = undefined;
215
63
  nativeAgentLoopStatePath = null;
216
64
  runtimeControlContext = null;
217
65
  sessionExecutionPolicy = null;
218
66
  lastSelectedRoute = null;
219
- activeTurn = null;
220
67
  constructor(deps) {
221
68
  this.deps = deps;
222
69
  this.groundingGateway = createChatGroundingGateway({
223
70
  stateManager: deps.stateManager,
224
71
  pluginLoader: deps.pluginLoader,
225
72
  });
73
+ this.eventBridge = new ChatRunnerEventBridge(() => this.onEvent ?? this.deps.onEvent);
74
+ this.commandHandler = new ChatRunnerCommandHandler({
75
+ deps: this.deps,
76
+ onNotification: this.onNotification,
77
+ getHistory: () => this.history,
78
+ setHistory: (history) => { this.history = history; },
79
+ getSessionCwd: () => this.sessionCwd,
80
+ setSessionCwd: (cwd) => { this.sessionCwd = cwd; },
81
+ setSessionActive: (active) => { this.sessionActive = active; },
82
+ getNativeAgentLoopStatePath: () => this.nativeAgentLoopStatePath,
83
+ setNativeAgentLoopStatePath: (path) => { this.nativeAgentLoopStatePath = path; },
84
+ getRuntimeControlContext: () => this.runtimeControlContext,
85
+ getPendingTend: () => this.pendingTend,
86
+ setPendingTend: (value) => { this.pendingTend = value; },
87
+ getLastSelectedRoute: () => this.lastSelectedRoute,
88
+ getSessionExecutionPolicy: () => this.getSessionExecutionPolicy(),
89
+ emitEvent: (event) => this.eventBridge.emitEvent(event),
90
+ getActiveSubscribers: () => this.activeSubscribers,
91
+ setSessionExecutionPolicy: (policy) => { this.sessionExecutionPolicy = policy; },
92
+ resetSessionExecutionPolicy: () => { this.sessionExecutionPolicy = null; },
93
+ });
226
94
  }
227
- /**
228
- * Initialize a persistent session for interactive (multi-turn) mode.
229
- * Must be called before the first execute() to share history across turns.
230
- * If not called, execute() auto-creates a new session per call (Phase 1a behavior).
231
- */
232
95
  startSession(cwd) {
233
96
  const gitRoot = resolveGitRoot(cwd);
234
97
  const sessionId = crypto.randomUUID();
@@ -236,13 +99,13 @@ export class ChatRunner {
236
99
  this.sessionCwd = gitRoot;
237
100
  this.sessionActive = true;
238
101
  this.nativeAgentLoopStatePath = `chat/agentloop/${sessionId}.state.json`;
239
- this.history.setAgentLoopStatePath(this.nativeAgentLoopStatePath);
102
+ this.history.resetAgentLoopState(this.nativeAgentLoopStatePath);
240
103
  this.sessionExecutionPolicy = null;
241
104
  }
242
105
  startSessionFromLoadedSession(session) {
243
- const chatSession = this.loadedSessionToChatSession(session);
106
+ const chatSession = loadedSessionToChatSession(session);
244
107
  this.history = ChatHistory.fromSession(this.deps.stateManager, chatSession);
245
- this.sessionCwd = session.cwd;
108
+ this.sessionCwd = resolveGitRoot(session.cwd);
246
109
  this.sessionActive = true;
247
110
  this.nativeAgentLoopStatePath = session.agentLoopStatePath ?? `chat/agentloop/${session.id}.state.json`;
248
111
  this.history.setAgentLoopStatePath(this.nativeAgentLoopStatePath);
@@ -255,17 +118,17 @@ export class ChatRunner {
255
118
  return this.history?.getMessages() ?? [];
256
119
  }
257
120
  hasActiveTurn() {
258
- return this.activeTurn !== null;
121
+ return this.eventBridge.hasActiveTurn();
259
122
  }
260
123
  async interruptAndRedirect(input, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
261
- const activeTurn = this.activeTurn;
124
+ const activeTurn = this.eventBridge.getActiveTurn();
262
125
  if (!activeTurn) {
263
126
  return this.execute(input, cwd, timeoutMs);
264
127
  }
265
128
  const start = Date.now();
266
129
  const redirect = classifyInterruptRedirect(input);
267
130
  if (redirect === "background") {
268
- return this.emitEphemeralAssistantResult(input, [
131
+ return this.eventBridge.emitEphemeralAssistantResult(input, [
269
132
  "Continuing this same turn in the background is not available yet.",
270
133
  "",
271
134
  "The active turn is still running in the foreground.",
@@ -276,20 +139,17 @@ export class ChatRunner {
276
139
  if (!activeTurn.abortController.signal.aborted) {
277
140
  activeTurn.abortController.abort();
278
141
  }
279
- this.emitCheckpoint("Interrupt requested", `Redirect: ${previewActivityText(input, 120)}`, activeTurn.context, "interrupt");
280
- const stopped = await this.waitForActiveTurn(activeTurn, 2_000);
142
+ this.eventBridge.emitCheckpoint("Interrupt requested", `Redirect: ${previewActivityText(input, 120)}`, activeTurn.context, "interrupt");
143
+ const stopped = await this.eventBridge.waitForActiveTurn(activeTurn, 2_000);
281
144
  if (!stopped) {
282
- return this.emitEphemeralAssistantResult(input, "Interrupt requested. The active turn will stop at the next safe point.", false, start);
283
- }
284
- if (redirect === "redirect") {
285
- return this.execute(input, cwd, timeoutMs);
145
+ return this.eventBridge.emitEphemeralAssistantResult(input, "Interrupt requested. The active turn will stop at the next safe point.", false, start);
286
146
  }
287
147
  let output;
288
148
  if (redirect === "diff") {
289
149
  const diff = await collectGitDiffArtifact(activeTurn.cwd);
290
150
  if (diff) {
291
- const context = this.createEventContext();
292
- this.emitDiffArtifact(diff, context);
151
+ const context = this.eventBridge.createEventContext();
152
+ this.eventBridge.emitDiffArtifact(diff, context);
293
153
  output = "Interrupted the active turn. Current diff is shown above.";
294
154
  }
295
155
  else {
@@ -297,8 +157,8 @@ export class ChatRunner {
297
157
  }
298
158
  }
299
159
  else if (redirect === "review") {
300
- const review = await this.handleReview(start);
301
- output = `Interrupted the active turn and switched to review-only mode.\n\n${review.output}`;
160
+ const review = await this.commandHandler.handleCommand("/review", activeTurn.cwd);
161
+ output = `Interrupted the active turn and switched to review-only mode.\n\n${review?.output ?? "Review unavailable."}`;
302
162
  }
303
163
  else {
304
164
  output = [
@@ -314,7 +174,7 @@ export class ChatRunner {
314
174
  "- Ask to show diff or switch to review if files may have changed.",
315
175
  ].join("\n");
316
176
  }
317
- return this.emitEphemeralAssistantResult(input, output, true, start);
177
+ return this.eventBridge.emitEphemeralAssistantResult(input, output, true, start);
318
178
  }
319
179
  setRuntimeControlContext(context) {
320
180
  this.runtimeControlContext = context;
@@ -323,2499 +183,296 @@ export class ChatRunner {
323
183
  if (!selectedRoute) {
324
184
  throw new Error("executeIngressMessage requires selectedRoute; use CrossPlatformChatSessionManager for ingress route selection.");
325
185
  }
326
- const runtimeControlContext = this.buildRuntimeControlContextFromIngress(ingress);
186
+ const runtimeControlContext = buildRuntimeControlContextFromIngress(ingress, this.runtimeControlContext, this.deps);
327
187
  return this.execute(ingress.text, cwd, timeoutMs, {
328
188
  selectedRoute,
329
189
  runtimeControlContext,
330
190
  goalId: ingress.goal_id,
331
191
  });
332
192
  }
333
- resolveRouteFromIngress(ingress) {
334
- return standaloneIngressRouter.selectRoute(ingress, this.getRouteCapabilities());
335
- }
336
- resolveRouteFromInput(input, runtimeControlContext) {
337
- return this.resolveRouteFromIngress(this.buildStandaloneIngressMessage(input, runtimeControlContext));
338
- }
339
- getRouteCapabilities() {
340
- return {
341
- hasLightweightLlm: this.deps.llmClient !== undefined,
342
- hasAgentLoop: this.deps.chatAgentLoopRunner !== undefined,
343
- hasToolLoop: this.deps.llmClient !== undefined,
344
- hasRuntimeControlService: this.deps.runtimeControlService !== undefined,
345
- hasDaemonTend: this.deps.llmClient !== undefined
346
- && this.deps.goalNegotiator !== undefined
347
- && this.deps.daemonClient !== undefined,
348
- };
349
- }
350
- buildStandaloneIngressMessage(input, runtimeControlContext) {
351
- const channel = runtimeControlContext?.replyTarget?.surface === "tui"
352
- ? "tui"
353
- : runtimeControlContext?.replyTarget?.surface === "cli"
354
- ? "cli"
355
- : runtimeControlContext?.replyTarget?.surface === "gateway"
356
- ? "plugin_gateway"
357
- : "cli";
358
- const runtimeApprovalFn = runtimeControlContext?.approvalFn
359
- ?? this.deps.runtimeControlApprovalFn
360
- ?? this.deps.approvalFn;
361
- const replyTarget = runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget;
362
- const replyTargetInput = replyTarget
363
- ? {
364
- ...(replyTarget.surface ? { surface: replyTarget.surface } : {}),
365
- channel,
366
- ...(replyTarget.platform ? { platform: replyTarget.platform } : {}),
367
- ...(replyTarget.conversation_id ? { conversation_id: replyTarget.conversation_id } : {}),
368
- ...(replyTarget.message_id ? { message_id: replyTarget.message_id } : {}),
369
- ...(replyTarget.response_channel ? { response_channel: replyTarget.response_channel } : {}),
370
- ...(replyTarget.outbox_topic ? { outbox_topic: replyTarget.outbox_topic } : {}),
371
- ...(replyTarget.identity_key ? { identity_key: replyTarget.identity_key } : {}),
372
- ...(replyTarget.user_id ? { user_id: replyTarget.user_id } : {}),
373
- ...(replyTarget.deliveryMode === "reply" || replyTarget.deliveryMode === "notify" || replyTarget.deliveryMode === "thread_reply"
374
- ? { deliveryMode: replyTarget.deliveryMode }
375
- : {}),
376
- ...(replyTarget.metadata ? { metadata: replyTarget.metadata } : {}),
377
- }
378
- : undefined;
379
- return buildStandaloneIngressMessage({
380
- text: input,
381
- channel,
382
- platform: runtimeControlContext?.replyTarget?.platform ?? this.deps.runtimeReplyTarget?.platform,
383
- identity_key: runtimeControlContext?.replyTarget?.identity_key ?? this.deps.runtimeReplyTarget?.identity_key,
384
- conversation_id: runtimeControlContext?.replyTarget?.conversation_id ?? this.deps.runtimeReplyTarget?.conversation_id,
385
- user_id: runtimeControlContext?.replyTarget?.user_id ?? this.deps.runtimeReplyTarget?.user_id,
386
- actor: runtimeControlContext?.actor ?? this.deps.runtimeControlActor,
387
- replyTarget: replyTargetInput,
388
- runtimeControl: {
389
- allowed: true,
390
- approvalMode: "interactive",
391
- },
392
- });
393
- }
394
- buildRuntimeControlContextFromIngress(ingress) {
395
- if (!ingress.actor && !ingress.replyTarget)
396
- return null;
397
- const interactiveApproval = this.runtimeControlContext?.approvalFn
398
- ?? this.deps.runtimeControlApprovalFn
399
- ?? this.deps.approvalFn;
400
- return {
401
- actor: ingress.actor,
402
- replyTarget: ingress.replyTarget,
403
- approvalFn: ingress.runtimeControl.approvalMode === "preapproved"
404
- ? async () => true
405
- : ingress.runtimeControl.approvalMode === "interactive"
406
- ? interactiveApproval
407
- : undefined,
408
- };
409
- }
410
- loadedSessionToChatSession(session) {
411
- return {
412
- id: session.id,
413
- cwd: session.cwd,
414
- createdAt: session.createdAt,
415
- updatedAt: session.updatedAt,
416
- messages: [...session.messages],
417
- ...(session.compactionSummary ? { compactionSummary: session.compactionSummary } : {}),
418
- ...(session.title ? { title: session.title } : {}),
419
- ...(session.agentLoopStatePath ? { agentLoopStatePath: session.agentLoopStatePath } : {}),
420
- ...(session.agentLoopStatus === "running" || session.agentLoopStatus === "completed" || session.agentLoopStatus === "failed"
421
- ? { agentLoopStatus: session.agentLoopStatus }
422
- : {}),
423
- ...(session.agentLoopResumable ? { agentLoopResumable: true } : {}),
424
- ...(session.agentLoopUpdatedAt ? { agentLoopUpdatedAt: session.agentLoopUpdatedAt } : {}),
425
- ...(session.agentLoop ? { agentLoop: session.agentLoop } : {}),
426
- };
427
- }
428
- formatRuntimeTimestamp(value) {
429
- return value ?? "unknown";
430
- }
431
- formatRuntimeTitle(value) {
432
- return value ? ` "${value}"` : "";
433
- }
434
- runtimeWarningLine(warnings) {
435
- return warnings.length > 0 ? `Warnings: ${warnings.length}` : null;
436
- }
437
- activeRuntimeSession(session) {
438
- return session.status === "active";
439
- }
440
- statusRuntimeRun(run) {
441
- return run.status === "queued"
442
- || run.status === "running"
443
- || run.status === "failed"
444
- || run.status === "timed_out"
445
- || run.status === "lost";
446
- }
447
- compactRunLine(run) {
448
- const title = this.formatRuntimeTitle(run.title);
449
- const updated = this.formatRuntimeTimestamp(run.updated_at ?? run.started_at ?? run.created_at);
450
- const summary = run.summary ? ` - ${run.summary.replace(/\s+/g, " ").trim()}` : "";
451
- const error = run.error ? ` - error: ${run.error.replace(/\s+/g, " ").trim()}` : "";
452
- return `- ${run.id}${title} [${run.kind}, ${run.status}], updated ${updated}${summary}${error}`;
453
- }
454
- compactSessionLine(session) {
455
- const displayId = session.kind === "conversation"
456
- ? session.transcript_ref?.id ?? session.id.replace(/^session:conversation:/, "")
457
- : session.id;
458
- const title = this.formatRuntimeTitle(session.title);
459
- const updated = this.formatRuntimeTimestamp(session.updated_at ?? session.last_event_at ?? session.created_at);
460
- const workspace = session.workspace ? `, cwd ${session.workspace}` : "";
461
- const resumable = session.resumable ? ", resumable" : "";
462
- const attachable = session.attachable ? ", attachable" : "";
463
- const runtimeId = displayId === session.id ? "" : `, runtime ${session.id}`;
464
- return `- ${displayId}${title} [${session.kind}, ${session.status}], updated ${updated}${workspace}${resumable}${attachable}${runtimeId}`;
465
- }
466
- formatRuntimeSessionsList(snapshot) {
467
- const chatSessions = snapshot.sessions.filter((session) => session.kind === "conversation");
468
- const nonChatSessions = snapshot.sessions.filter((session) => session.kind !== "conversation");
469
- const lines = ["Chat sessions:"];
470
- if (chatSessions.length === 0) {
471
- lines.push("No chat sessions found.");
193
+ async execute(input, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
194
+ const eventContext = this.eventBridge.createEventContext();
195
+ const resolvedCwd = resolveGitRoot(cwd);
196
+ const activeTurn = this.eventBridge.beginActiveTurn(eventContext, resolvedCwd);
197
+ const resumeCommand = this.commandHandler.parseResumeCommand(input);
198
+ const resumeOnly = resumeCommand !== null;
199
+ const runtimeControlContext = options.runtimeControlContext ?? this.runtimeControlContext;
200
+ const executionGoalId = options.goalId ?? this.deps.goalId;
201
+ const commandResult = resumeOnly ? null : await this.commandHandler.handleCommand(input, resolvedCwd);
202
+ if (commandResult !== null) {
203
+ return this.finalizeNonPersistentResult(commandResult, eventContext);
472
204
  }
473
- else {
474
- for (const session of chatSessions) {
475
- lines.push(this.compactSessionLine(session));
476
- const runs = snapshot.background_runs.filter((run) => run.parent_session_id === session.id);
477
- for (const run of runs) {
478
- lines.push(` ${this.compactRunLine(run)}`);
205
+ if (this.pendingTend !== null && !resumeOnly) {
206
+ const confirmationResult = await this.commandHandler.handleTendConfirmation(input.trim(), Date.now());
207
+ return this.finalizeNonPersistentResult(confirmationResult, eventContext);
208
+ }
209
+ if (resumeOnly && resumeCommand.selector) {
210
+ try {
211
+ const selectorResolution = await resolveChatResumeSelector(resumeCommand.selector, this.deps);
212
+ if (selectorResolution.nonResumableMessage) {
213
+ return this.finalizeNonPersistentResult({
214
+ success: false,
215
+ output: selectorResolution.nonResumableMessage,
216
+ elapsed_ms: 0,
217
+ }, eventContext);
218
+ }
219
+ const catalog = new ChatSessionCatalog(this.deps.stateManager);
220
+ const session = await catalog.loadSessionBySelector(selectorResolution.chatSelector);
221
+ if (!session) {
222
+ return this.finalizeNonPersistentResult({
223
+ success: false,
224
+ output: `No chat session matched selector "${selectorResolution.chatSelector}".`,
225
+ elapsed_ms: 0,
226
+ }, eventContext);
479
227
  }
228
+ this.startSessionFromLoadedSession(session);
229
+ }
230
+ catch (err) {
231
+ const output = err instanceof ChatSessionSelectorError ? err.message : `Failed to load chat session: ${err instanceof Error ? err.message : String(err)}`;
232
+ return this.finalizeNonPersistentResult({ success: false, output, elapsed_ms: 0 }, eventContext);
480
233
  }
481
234
  }
482
- if (nonChatSessions.length > 0) {
483
- lines.push("", "Other runtime sessions:");
484
- lines.push(...nonChatSessions.map((session) => this.compactSessionLine(session)));
485
- }
486
- if (snapshot.background_runs.length > 0) {
487
- lines.push("", "Background runs:");
488
- lines.push(...snapshot.background_runs.map((run) => this.compactRunLine(run)));
489
- }
490
- const warningLine = this.runtimeWarningLine(snapshot.warnings);
491
- if (warningLine)
492
- lines.push("", warningLine);
493
- return lines.join("\n");
494
- }
495
- formatRuntimeStatus(snapshot) {
496
- const activeSessions = snapshot.sessions.filter((session) => this.activeRuntimeSession(session));
497
- const statusRuns = snapshot.background_runs.filter((run) => this.statusRuntimeRun(run));
498
- const lines = [];
499
- if (activeSessions.length > 0) {
500
- lines.push("Active runtime sessions:");
501
- lines.push(...activeSessions.map((session) => this.compactSessionLine(session)));
502
- }
503
- if (statusRuns.length > 0) {
504
- if (lines.length > 0)
505
- lines.push("");
506
- lines.push("Background runs (queued/running/attention-needed):");
507
- lines.push(...statusRuns.map((run) => this.compactRunLine(run)));
508
- }
509
- const warningLine = this.runtimeWarningLine(snapshot.warnings);
510
- if (warningLine) {
511
- if (lines.length > 0)
512
- lines.push("");
513
- lines.push(warningLine);
235
+ if (!this.sessionActive) {
236
+ const sessionId = crypto.randomUUID();
237
+ this.history = new ChatHistory(this.deps.stateManager, sessionId, resolvedCwd);
238
+ this.nativeAgentLoopStatePath = `chat/agentloop/${sessionId}.state.json`;
239
+ this.history.resetAgentLoopState(this.nativeAgentLoopStatePath);
514
240
  }
515
- return lines.length > 0 ? lines.join("\n") : "No active runtime sessions or running/failed/lost background runs found.";
516
- }
517
- formatHistory(session) {
518
- const title = session.title ? ` "${session.title}"` : "";
519
- if (session.messages.length === 0) {
520
- return `Session ${session.id}${title} has no messages.`;
241
+ const executionCwd = this.sessionCwd ?? resolvedCwd;
242
+ const gitRoot = this.sessionCwd ?? resolvedCwd;
243
+ activeTurn.cwd = gitRoot;
244
+ const history = this.history;
245
+ const pinnedReplyTarget = normalizePinnedReplyTarget(runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget ?? null);
246
+ if (pinnedReplyTarget) {
247
+ history.setNotificationReplyTarget(pinnedReplyTarget);
521
248
  }
522
- const lines = session.messages.map((message) => {
523
- const role = message.role === "assistant" ? "Assistant" : "User";
524
- return `${role}: ${message.content}`;
249
+ this.eventBridge.emitEvent({
250
+ type: "lifecycle_start",
251
+ input,
252
+ ...this.eventBridge.eventBase(eventContext),
525
253
  });
526
- return `Session ${session.id}${title} (${session.cwd})\n${lines.join("\n")}`;
527
- }
528
- async loadGoals() {
529
- const goalIds = await this.deps.stateManager.listGoalIds();
530
- const goals = await Promise.all(goalIds.map((id) => this.deps.stateManager.loadGoal(id)));
531
- return goals.filter((goal) => goal !== null);
532
- }
533
- async listAllGoalIds() {
534
- const activeIds = await this.deps.stateManager.listGoalIds();
535
- const archivedIds = await this.deps.stateManager.listArchivedGoals();
536
- const recoverableArchivedIds = await this.listRecoverableArchivedGoalIds();
537
- return [...new Set([...activeIds, ...archivedIds, ...recoverableArchivedIds])];
538
- }
539
- resolveStatePath(baseDir, ...segments) {
540
- const base = path.resolve(baseDir);
541
- const resolved = path.resolve(base, ...segments);
542
- if (!resolved.startsWith(base + path.sep))
543
- return null;
544
- return resolved;
545
- }
546
- async listRecoverableArchivedGoalIds() {
547
- const stateManager = this.deps.stateManager;
548
- if (typeof stateManager.getBaseDir !== "function")
549
- return [];
550
- const archiveDir = this.resolveStatePath(stateManager.getBaseDir(), "archive");
551
- if (archiveDir === null)
552
- return [];
553
- let entries = [];
554
- try {
555
- entries = await fsp.readdir(archiveDir, { withFileTypes: true });
556
- }
557
- catch {
558
- return [];
254
+ if (!resumeOnly) {
255
+ await history.appendUserMessage(input);
559
256
  }
560
- const goalIds = [];
561
- for (const entry of entries) {
562
- if (!entry.isDirectory() || entry.name === ".staging")
563
- continue;
257
+ if (this.cachedStaticSystemPrompt === null) {
564
258
  try {
565
- await fsp.access(path.join(archiveDir, entry.name, "goal", "goal.json"));
566
- goalIds.push(entry.name);
259
+ this.cachedStaticSystemPrompt = buildStaticSystemPrompt(this.providerConfigBaseDir());
567
260
  }
568
261
  catch {
569
- continue;
570
- }
571
- }
572
- return goalIds;
573
- }
574
- activeGoals(goals) {
575
- return goals.filter((goal) => goal.status === "active" || goal.status === "waiting" || goal.loop_status === "running");
576
- }
577
- formatGoalLine(goal) {
578
- const dimensions = goal.dimensions.length === 0
579
- ? "no dimensions"
580
- : goal.dimensions
581
- .slice(0, 3)
582
- .map((dimension) => `${dimension.name}: ${String(dimension.current_value)} target ${JSON.stringify(dimension.threshold)}`)
583
- .join("; ");
584
- return `${goal.id} - ${goal.title} [${goal.status}, loop ${goal.loop_status}] ${dimensions}`;
585
- }
586
- async handleStatus(args, start) {
587
- if (args) {
588
- const goal = await this.deps.stateManager.loadGoal(args);
589
- if (!goal) {
590
- return { success: false, output: `Goal not found: ${args}`, elapsed_ms: Date.now() - start };
262
+ this.cachedStaticSystemPrompt = "";
591
263
  }
592
- const lines = [
593
- `Goal status: ${goal.title}`,
594
- `ID: ${goal.id}`,
595
- `Status: ${goal.status}`,
596
- `Loop: ${goal.loop_status}`,
597
- `Updated: ${goal.updated_at}`,
598
- `Children: ${goal.children_ids.length}`,
599
- `Dimensions:`,
600
- ...goal.dimensions.map((dimension) => `- ${dimension.name}: current=${String(dimension.current_value)}, threshold=${JSON.stringify(dimension.threshold)}, confidence=${dimension.confidence}`),
601
- ];
602
- return { success: true, output: lines.join("\n"), elapsed_ms: Date.now() - start };
603
264
  }
604
- const registry = createRuntimeSessionRegistry({ stateManager: this.deps.stateManager });
605
- const [goals, runtimeSnapshot] = await Promise.all([
606
- this.loadGoals(),
607
- registry.snapshot(),
608
- ]);
609
- const active = this.activeGoals(goals);
610
- const runtimeStatus = this.formatRuntimeStatus(runtimeSnapshot);
611
- if (active.length === 0) {
612
- return { success: true, output: `No active goals found.\n\n${runtimeStatus}`, elapsed_ms: Date.now() - start };
613
- }
614
- return {
615
- success: true,
616
- output: `Active goals:\n${active.map((goal) => this.formatGoalLine(goal)).join("\n")}\n\n${runtimeStatus}`,
617
- elapsed_ms: Date.now() - start,
618
- };
619
- }
620
- async handleGoals(start) {
621
- const goals = await this.loadGoals();
622
- if (goals.length === 0) {
623
- return { success: true, output: "No goals found.", elapsed_ms: Date.now() - start };
265
+ const messages = history.getMessages();
266
+ const compactionSummary = history.getSessionData().compactionSummary;
267
+ const priorTurns = resumeOnly ? messages.slice(-10) : messages.slice(0, -1).slice(-10);
268
+ const historySections = [];
269
+ if (compactionSummary) {
270
+ historySections.push(`Compacted previous conversation summary:\n${compactionSummary}`);
624
271
  }
625
- return {
626
- success: true,
627
- output: `Goals:\n${goals.map((goal) => this.formatGoalLine(goal)).join("\n")}`,
628
- elapsed_ms: Date.now() - start,
629
- };
630
- }
631
- async readTasksFromDir(tasksDir) {
632
- let entries = [];
633
- try {
634
- entries = await fsp.readdir(tasksDir);
272
+ if (priorTurns.length > 0) {
273
+ const lines = priorTurns.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
274
+ historySections.push(`Previous conversation:\n${lines}`);
635
275
  }
636
- catch {
637
- return [];
276
+ const historyBlock = historySections.length > 0 ? `${historySections.join("\n\n")}\n\nCurrent message:\n` : "";
277
+ const selectedRoute = resumeOnly
278
+ ? null
279
+ : (options.selectedRoute ?? this.resolveRouteFromInput(input, runtimeControlContext));
280
+ this.lastSelectedRoute = selectedRoute;
281
+ this.eventBridge.emitIntent(input, selectedRoute, eventContext);
282
+ const start = Date.now();
283
+ const assistantBuffer = { text: "" };
284
+ const identityResponse = resumeOnly ? null : resolveSelfIdentityResponse(input, this.providerConfigBaseDir());
285
+ if (identityResponse !== null) {
286
+ const elapsed_ms = Date.now() - start;
287
+ await history.appendAssistantMessage(identityResponse);
288
+ this.eventBridge.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
289
+ this.eventBridge.emitEvent({
290
+ type: "assistant_final",
291
+ text: identityResponse,
292
+ persisted: true,
293
+ ...this.eventBridge.eventBase(eventContext),
294
+ });
295
+ this.eventBridge.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
296
+ return {
297
+ success: true,
298
+ output: identityResponse,
299
+ elapsed_ms,
300
+ };
638
301
  }
639
- const tasks = [];
640
- for (const entry of entries) {
641
- if (!entry.endsWith(".json") || entry === "task-history.json" || entry === "last-failure-context.json")
642
- continue;
643
- let raw;
644
- try {
645
- raw = JSON.parse(await fsp.readFile(path.join(tasksDir, entry), "utf-8"));
302
+ if (selectedRoute?.kind === "runtime_control") {
303
+ this.eventBridge.emitCheckpoint("Runtime control selected", `${selectedRoute.intent.kind} request recognized.`, eventContext, "route");
304
+ const runtimeControlResult = await executeRuntimeControlRoute(this.routeHost(), selectedRoute, runtimeControlContext, executionCwd, start);
305
+ if (runtimeControlResult.success) {
306
+ await history.appendAssistantMessage(runtimeControlResult.output);
307
+ this.eventBridge.emitCheckpoint("Runtime control completed", "The runtime-control operation produced a result.", eventContext, "complete");
308
+ this.eventBridge.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
309
+ this.eventBridge.emitEvent({
310
+ type: "assistant_final",
311
+ text: runtimeControlResult.output,
312
+ persisted: true,
313
+ ...this.eventBridge.eventBase(eventContext),
314
+ });
315
+ this.eventBridge.emitLifecycleEndEvent("completed", runtimeControlResult.elapsed_ms, eventContext, true);
646
316
  }
647
- catch {
648
- continue;
317
+ else {
318
+ runtimeControlResult.output = this.eventBridge.emitLifecycleErrorEvent(runtimeControlResult.output, assistantBuffer.text, eventContext);
319
+ this.eventBridge.emitLifecycleEndEvent("error", runtimeControlResult.elapsed_ms, eventContext, false);
649
320
  }
650
- const parsed = TaskSchema.safeParse(raw);
651
- if (parsed.success)
652
- tasks.push(parsed.data);
653
- }
654
- return tasks.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
655
- }
656
- async readTasksForGoal(goalId) {
657
- const stateManager = this.deps.stateManager;
658
- if (typeof stateManager.getBaseDir !== "function")
659
- return [];
660
- const baseDir = stateManager.getBaseDir();
661
- const activeTasksDir = this.resolveStatePath(baseDir, "tasks", goalId);
662
- const archiveTasksDir = this.resolveStatePath(baseDir, "archive", goalId, "tasks");
663
- if (activeTasksDir === null || archiveTasksDir === null)
664
- return [];
665
- const activeTasks = await this.readTasksFromDir(activeTasksDir);
666
- if (activeTasks.length > 0)
667
- return activeTasks;
668
- return this.readTasksFromDir(archiveTasksDir);
669
- }
670
- async resolveGoalForTasks(selector) {
671
- if (selector)
672
- return { goalId: selector };
673
- const active = this.activeGoals(await this.loadGoals());
674
- if (active.length === 1)
675
- return { goalId: active[0].id };
676
- if (active.length === 0)
677
- return { error: "No active goals found. Use /tasks <goal-id>." };
678
- return { error: "Multiple active goals found. Use /tasks <goal-id>." };
679
- }
680
- formatTaskLine(task) {
681
- const verdict = task.verification_verdict ? `, verdict ${task.verification_verdict}` : "";
682
- return `${task.id} - ${task.status}${verdict}: ${task.work_description}`;
683
- }
684
- async handleTasks(args, start) {
685
- const resolved = await this.resolveGoalForTasks(args);
686
- if (resolved.error || !resolved.goalId) {
687
- return { success: false, output: resolved.error ?? "Usage: /tasks <goal-id>", elapsed_ms: Date.now() - start };
688
- }
689
- const tasks = await this.readTasksForGoal(resolved.goalId);
690
- if (tasks.length === 0) {
691
- return { success: true, output: `No tasks found for goal "${resolved.goalId}".`, elapsed_ms: Date.now() - start };
692
- }
693
- return {
694
- success: true,
695
- output: `Tasks for goal ${resolved.goalId}:\n${tasks.map((task) => this.formatTaskLine(task)).join("\n")}`,
696
- elapsed_ms: Date.now() - start,
697
- };
698
- }
699
- parseTaskArgs(args) {
700
- const parts = args.split(/\s+/).filter(Boolean);
701
- const goalFlagIndex = parts.indexOf("--goal");
702
- if (goalFlagIndex >= 0) {
703
- const goalId = parts[goalFlagIndex + 1];
704
- parts.splice(goalFlagIndex, goalId ? 2 : 1);
705
- return { taskId: parts[0], goalId };
321
+ return runtimeControlResult;
706
322
  }
707
- return { taskId: parts[0], goalId: parts[1] };
708
- }
709
- async findTask(taskId, goalId) {
710
- const goalIds = goalId ? [goalId] : await this.listAllGoalIds();
711
- const matches = [];
712
- for (const candidateGoalId of goalIds) {
713
- let raw = null;
323
+ const usesNativeAgentLoop = resumeOnly || selectedRoute?.kind === "agent_loop";
324
+ const groundingWorkspaceContext = !resumeOnly && usesNativeAgentLoop
325
+ ? await buildChatContext(input, executionCwd)
326
+ : undefined;
327
+ let systemPrompt = this.cachedStaticSystemPrompt ?? "";
328
+ if (!resumeOnly) {
714
329
  try {
715
- raw = await this.deps.stateManager.readRaw(`tasks/${candidateGoalId}/${taskId}.json`);
330
+ this.eventBridge.emitActivity("lifecycle", "Preparing context...", eventContext, "lifecycle:context");
331
+ if (usesNativeAgentLoop) {
332
+ systemPrompt = await buildChatAgentLoopSystemPrompt({
333
+ stateManager: this.deps.stateManager,
334
+ pluginLoader: this.deps.pluginLoader,
335
+ workspaceRoot: executionCwd,
336
+ goalId: executionGoalId,
337
+ userMessage: input,
338
+ trustProjectInstructions: this.sessionExecutionPolicy?.trustProjectInstructions ?? true,
339
+ workspaceContext: groundingWorkspaceContext,
340
+ });
341
+ }
342
+ else {
343
+ const groundingBundle = await this.groundingGateway.build({
344
+ surface: "chat",
345
+ purpose: "general_turn",
346
+ workspaceRoot: executionCwd,
347
+ goalId: executionGoalId,
348
+ userMessage: input,
349
+ query: input,
350
+ trustProjectInstructions: this.sessionExecutionPolicy?.trustProjectInstructions ?? true,
351
+ });
352
+ systemPrompt = String(groundingBundle.render("prompt"));
353
+ }
716
354
  }
717
355
  catch {
718
- raw = null;
719
- }
720
- if (!raw) {
721
- const tasks = await this.readTasksForGoal(candidateGoalId);
722
- const matched = tasks.find((task) => task.id === taskId || task.id.startsWith(taskId));
723
- if (matched)
724
- matches.push({ goalId: candidateGoalId, task: matched });
725
- continue;
356
+ systemPrompt = this.cachedStaticSystemPrompt ?? "";
726
357
  }
727
- const parsed = TaskSchema.safeParse(raw);
728
- if (parsed.success)
729
- matches.push({ goalId: candidateGoalId, task: parsed.data });
358
+ this.eventBridge.emitCheckpoint("Context gathered", usesNativeAgentLoop
359
+ ? "Workspace and agent-loop grounding are ready."
360
+ : "Workspace grounding is ready.", eventContext, "context");
730
361
  }
731
- return { task: matches.length === 1 ? matches[0].task : undefined, matches };
732
- }
733
- formatTask(task) {
734
- const lines = [
735
- `Task: ${task.id}`,
736
- `Goal: ${task.goal_id}`,
737
- `Status: ${task.status}`,
738
- `Category: ${task.task_category}`,
739
- `Created: ${task.created_at}`,
740
- `Work: ${task.work_description}`,
741
- `Approach: ${task.approach}`,
742
- ];
743
- if (task.started_at)
744
- lines.push(`Started: ${task.started_at}`);
745
- if (task.completed_at)
746
- lines.push(`Completed: ${task.completed_at}`);
747
- if (task.verification_verdict)
748
- lines.push(`Verification: ${task.verification_verdict}`);
749
- if (task.verification_evidence?.length)
750
- lines.push(`Evidence: ${task.verification_evidence.join("; ")}`);
751
- if (task.success_criteria.length > 0) {
752
- lines.push("Success criteria:");
753
- lines.push(...task.success_criteria.map((criterion) => `- ${criterion.description}`));
362
+ const agentLoopSystemPrompt = [
363
+ systemPrompt,
364
+ compactionSummary ? `## Compacted Chat Summary\n${compactionSummary}` : "",
365
+ ]
366
+ .filter((section) => section && section.trim().length > 0)
367
+ .join("\n\n")
368
+ .trim();
369
+ const context = resumeOnly || usesNativeAgentLoop ? "" : await buildChatContext(input, gitRoot);
370
+ const basePrompt = resumeOnly ? "" : (context ? `${context}\n\n${input}` : input);
371
+ const prompt = historyBlock ? `${historyBlock}${basePrompt}` : basePrompt;
372
+ if (resumeOnly && !this.deps.chatAgentLoopRunner) {
373
+ const elapsed_ms = Date.now() - start;
374
+ const output = this.eventBridge.emitLifecycleErrorEvent("Resume requires the native chat agentloop runtime.", assistantBuffer.text, eventContext);
375
+ this.eventBridge.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
376
+ return { success: false, output, elapsed_ms };
754
377
  }
755
- return lines.join("\n");
756
- }
757
- async handleTask(args, start) {
758
- const { taskId, goalId } = this.parseTaskArgs(args);
759
- if (!taskId) {
760
- return { success: false, output: "Usage: /task <task-id> [goal-id]", elapsed_ms: Date.now() - start };
378
+ if (resumeOnly || selectedRoute?.kind === "agent_loop") {
379
+ return executeAgentLoopRoute(this.routeHost(), {
380
+ resumeOnly,
381
+ executionCwd,
382
+ executionGoalId,
383
+ basePrompt,
384
+ priorTurns,
385
+ agentLoopSystemPrompt,
386
+ assistantBuffer,
387
+ eventContext,
388
+ history,
389
+ gitRoot,
390
+ activeAbortSignal: activeTurn.abortController.signal,
391
+ start,
392
+ });
761
393
  }
762
- const found = await this.findTask(taskId, goalId);
763
- if (found.matches.length > 1) {
764
- return {
765
- success: false,
766
- output: `Task selector "${taskId}" matched multiple goals. Use /task ${taskId} <goal-id>.\n${found.matches.map((match) => `- ${match.goalId}`).join("\n")}`,
767
- elapsed_ms: Date.now() - start,
768
- };
394
+ if (selectedRoute?.kind === "tool_loop") {
395
+ return executeToolLoopRoute(this.routeHost(), {
396
+ prompt,
397
+ eventContext,
398
+ assistantBuffer,
399
+ systemPrompt: systemPrompt || undefined,
400
+ executionGoalId,
401
+ history,
402
+ gitRoot,
403
+ start,
404
+ });
769
405
  }
770
- if (!found.task) {
771
- const suffix = goalId ? ` for goal "${goalId}"` : "";
772
- return { success: false, output: `Task not found: ${taskId}${suffix}`, elapsed_ms: Date.now() - start };
406
+ if (!resumeOnly && selectedRoute && selectedRoute.kind !== "adapter") {
407
+ const elapsed_ms = Date.now() - start;
408
+ const output = this.eventBridge.emitLifecycleErrorEvent(`Unsupported chat route: ${selectedRoute.kind}`, assistantBuffer.text, eventContext);
409
+ this.eventBridge.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
410
+ return { success: false, output, elapsed_ms };
773
411
  }
774
- return { success: true, output: this.formatTask(found.task), elapsed_ms: Date.now() - start };
775
- }
776
- providerConfigBaseDir() {
777
- const stateManager = this.deps.stateManager;
778
- return typeof stateManager.getBaseDir === "function" ? stateManager.getBaseDir() : getPulseedDirPath();
779
- }
780
- async readProviderConfigSummary() {
781
- const config = await loadProviderConfig({
782
- baseDir: this.providerConfigBaseDir(),
783
- saveMigration: false,
412
+ return executeAdapterRoute(this.routeHost(), {
413
+ prompt,
414
+ cwd,
415
+ timeoutMs,
416
+ systemPrompt: systemPrompt || undefined,
417
+ eventContext,
418
+ assistantBuffer,
419
+ gitRoot,
420
+ start,
421
+ history,
784
422
  });
785
- return {
786
- provider: config.provider,
787
- model: config.model,
788
- adapter: config.adapter,
789
- light_model: config.light_model,
790
- base_url: config.base_url,
791
- codex_cli_path: config.codex_cli_path,
792
- has_api_key: Boolean(config.api_key),
793
- };
794
423
  }
795
- formatConfig(config) {
796
- return Object.entries(config)
797
- .filter(([, value]) => value !== undefined)
798
- .map(([key, value]) => `${key}: ${typeof value === "string" && /key|token|secret/i.test(key) ? "[masked]" : String(value)}`)
799
- .join("\n");
424
+ getSessionCwd() {
425
+ return this.sessionCwd;
800
426
  }
801
- async handleConfig(start) {
802
- const config = await this.readProviderConfigSummary();
803
- return { success: true, output: `Provider configuration:\n${this.formatConfig(config)}`, elapsed_ms: Date.now() - start };
427
+ getNativeAgentLoopStatePath() {
428
+ return this.nativeAgentLoopStatePath;
804
429
  }
805
- async handleModel(start) {
806
- const config = await this.readProviderConfigSummary();
807
- return {
808
- success: true,
809
- output: `Model: ${config.model}\nProvider: ${config.provider}\nAdapter: ${config.adapter}`,
810
- elapsed_ms: Date.now() - start,
811
- };
430
+ setSessionExecutionPolicy(policy) {
431
+ this.sessionExecutionPolicy = policy;
812
432
  }
813
- async handlePlugins(start) {
814
- if (!this.deps.pluginLoader) {
815
- return { success: true, output: "Plugin information is not available in this chat session.", elapsed_ms: Date.now() - start };
816
- }
817
- try {
818
- const plugins = await this.deps.pluginLoader.loadAll();
819
- if (plugins.length === 0) {
820
- return { success: true, output: "No plugins found.", elapsed_ms: Date.now() - start };
821
- }
822
- return {
823
- success: true,
824
- output: `Plugins:\n${plugins.map((plugin) => `${plugin.name} - ${plugin.type ?? "unknown"} - ${plugin.enabled === false ? "disabled" : "enabled"}`).join("\n")}`,
825
- elapsed_ms: Date.now() - start,
826
- };
827
- }
828
- catch (err) {
829
- const message = err instanceof Error ? err.message : String(err);
830
- return { success: true, output: `Plugin information is unavailable: ${message}`, elapsed_ms: Date.now() - start };
831
- }
433
+ async getSessionExecutionPolicy() {
434
+ const policy = await resolveSessionExecutionPolicy(this.sessionExecutionPolicy, this.sessionCwd);
435
+ this.sessionExecutionPolicy = policy;
436
+ return policy;
832
437
  }
833
- zeroUsageCounter() {
834
- return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
438
+ resolveRouteFromIngress(ingress) {
439
+ return standaloneIngressRouter.selectRoute(ingress, getRouteCapabilities(this.deps));
440
+ }
441
+ resolveRouteFromInput(input, runtimeControlContext) {
442
+ return this.resolveRouteFromIngress(buildStandaloneIngressMessageFromContext(input, runtimeControlContext, this.deps));
835
443
  }
836
- normalizeUsageCounter(usage) {
837
- const inputTokens = Number.isFinite(usage.inputTokens) ? Math.max(0, Math.floor(usage.inputTokens)) : 0;
838
- const outputTokens = Number.isFinite(usage.outputTokens) ? Math.max(0, Math.floor(usage.outputTokens)) : 0;
839
- const totalTokens = Number.isFinite(usage.totalTokens)
840
- ? Math.max(0, Math.floor(usage.totalTokens))
841
- : inputTokens + outputTokens;
842
- return { inputTokens, outputTokens, totalTokens };
444
+ loadedSessionToChatSession(session) {
445
+ return loadedSessionToChatSession(session);
843
446
  }
844
- usageFromLLMResponse(response) {
845
- const inputTokens = response.usage?.input_tokens ?? 0;
846
- const outputTokens = response.usage?.output_tokens ?? 0;
447
+ routeHost() {
847
448
  return {
848
- inputTokens,
849
- outputTokens,
850
- totalTokens: inputTokens + outputTokens,
449
+ deps: this.deps,
450
+ eventBridge: this.eventBridge,
451
+ activatedTools: this.activatedTools,
452
+ getConversationSessionId: () => this.history?.getSessionId() ?? null,
453
+ getSessionCwd: () => this.sessionCwd,
454
+ getNativeAgentLoopStatePath: () => this.nativeAgentLoopStatePath,
455
+ getSessionExecutionPolicy: () => this.getSessionExecutionPolicy(),
456
+ setSessionExecutionPolicy: (policy) => { this.sessionExecutionPolicy = policy; },
851
457
  };
852
458
  }
853
- addUsageCounter(target, delta) {
854
- const normalizedDelta = this.normalizeUsageCounter(delta);
855
- target.inputTokens += normalizedDelta.inputTokens;
856
- target.outputTokens += normalizedDelta.outputTokens;
857
- target.totalTokens += normalizedDelta.totalTokens;
858
- }
859
- hasUsage(usage) {
860
- return usage.totalTokens > 0 || usage.inputTokens > 0 || usage.outputTokens > 0;
861
- }
862
- formatUsageCounter(prefix, usage) {
863
- return [
864
- `${prefix} input tokens: ${usage.inputTokens}`,
865
- `${prefix} output tokens: ${usage.outputTokens}`,
866
- `${prefix} total tokens: ${usage.totalTokens}`,
867
- ];
868
- }
869
- parseUsagePeriodMs(period) {
870
- const match = /^(\d+)([dhw])$/i.exec(period.trim());
871
- if (!match) {
872
- throw new Error("period must be one of 24h, 7d, 2w");
873
- }
874
- const value = Number(match[1]);
875
- const unit = match[2]?.toLowerCase();
876
- if (!Number.isFinite(value) || value <= 0) {
877
- throw new Error("period value must be positive");
878
- }
879
- if (unit === "h")
880
- return value * 60 * 60 * 1000;
881
- if (unit === "w")
882
- return value * 7 * 24 * 60 * 60 * 1000;
883
- return value * 24 * 60 * 60 * 1000;
459
+ providerConfigBaseDir() {
460
+ const stateManager = this.deps.stateManager;
461
+ return typeof stateManager.getBaseDir === "function" ? stateManager.getBaseDir() : getPulseedDirPath();
884
462
  }
885
- async collectGoalUsage(goalId) {
886
- const baseDir = this.deps.stateManager.getBaseDir();
887
- const ledgerDir = path.join(baseDir, "tasks", goalId, "ledger");
888
- let entries = [];
889
- try {
890
- entries = await fsp.readdir(ledgerDir);
891
- }
892
- catch (err) {
893
- if (err.code !== "ENOENT")
894
- throw err;
895
- return { goalId, totalTokens: 0, taskCount: 0, terminalTaskCount: 0 };
463
+ finalizeNonPersistentResult(result, eventContext) {
464
+ if (result.output) {
465
+ this.eventBridge.emitEvent({
466
+ type: "assistant_final",
467
+ text: result.output,
468
+ persisted: false,
469
+ ...this.eventBridge.eventBase(eventContext),
470
+ });
896
471
  }
897
- let totalTokens = 0;
898
- let taskCount = 0;
899
- let terminalTaskCount = 0;
900
- for (const entry of entries) {
901
- if (!entry.endsWith(".json"))
902
- continue;
903
- taskCount += 1;
904
- try {
905
- const raw = await fsp.readFile(path.join(ledgerDir, entry), "utf-8");
906
- const parsed = JSON.parse(raw);
907
- if (typeof parsed.summary?.tokens_used === "number") {
908
- totalTokens += parsed.summary.tokens_used;
909
- }
910
- if (parsed.summary?.latest_event_type === "succeeded"
911
- || parsed.summary?.latest_event_type === "failed"
912
- || parsed.summary?.latest_event_type === "abandoned") {
913
- terminalTaskCount += 1;
914
- }
915
- }
916
- catch {
917
- // Ignore malformed records.
918
- }
919
- }
920
- return { goalId, totalTokens, taskCount, terminalTaskCount };
921
- }
922
- async collectScheduleUsage(period) {
923
- const periodMs = this.parseUsagePeriodMs(period);
924
- const since = Date.now() - periodMs;
925
- const historyPath = path.join(this.deps.stateManager.getBaseDir(), "schedule-history.json");
926
- let raw;
927
- try {
928
- raw = JSON.parse(await fsp.readFile(historyPath, "utf-8"));
929
- }
930
- catch (err) {
931
- if (err.code === "ENOENT") {
932
- return { period, runs: 0, totalTokens: 0 };
933
- }
934
- throw err;
935
- }
936
- if (!Array.isArray(raw)) {
937
- return { period, runs: 0, totalTokens: 0 };
938
- }
939
- let runs = 0;
940
- let totalTokens = 0;
941
- for (const record of raw) {
942
- if (!record || typeof record !== "object")
943
- continue;
944
- const finishedAt = record["finished_at"];
945
- const firedAt = typeof finishedAt === "string" ? Date.parse(finishedAt) : Number.NaN;
946
- if (!Number.isFinite(firedAt) || firedAt < since)
947
- continue;
948
- runs += 1;
949
- const tokensUsed = record["tokens_used"];
950
- if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
951
- totalTokens += tokensUsed;
952
- }
953
- }
954
- return { period, runs, totalTokens };
955
- }
956
- async handleUsage(args, start) {
957
- const tokens = args.trim().split(/\s+/).filter(Boolean);
958
- const scope = tokens[0]?.toLowerCase();
959
- if (!scope || scope === "session") {
960
- if (!this.history) {
961
- return { success: false, output: "No active chat session. Start a session and run work before /usage.", elapsed_ms: Date.now() - start };
962
- }
963
- const session = this.history.getSessionData();
964
- const totals = this.normalizeUsageCounter(session.usage?.totals ?? this.zeroUsageCounter());
965
- const lines = [
966
- `Usage summary (session ${session.id})`,
967
- ...this.formatUsageCounter("Session", totals),
968
- ];
969
- const phaseEntries = Object.entries(session.usage?.byPhase ?? {})
970
- .map(([phase, usage]) => ({ phase, usage: this.normalizeUsageCounter(usage) }))
971
- .filter((entry) => this.hasUsage(entry.usage))
972
- .sort((left, right) => right.usage.totalTokens - left.usage.totalTokens);
973
- if (phaseEntries.length > 0) {
974
- lines.push("");
975
- lines.push("By phase:");
976
- for (const entry of phaseEntries) {
977
- lines.push(`- ${entry.phase}: ${entry.usage.totalTokens} (in=${entry.usage.inputTokens}, out=${entry.usage.outputTokens})`);
978
- }
979
- }
980
- return { success: true, output: lines.join("\n"), elapsed_ms: Date.now() - start };
981
- }
982
- if (scope === "goal" || scope === "daemon") {
983
- const goalId = tokens[1] ?? this.deps.goalId;
984
- if (!goalId) {
985
- return { success: false, output: "Usage: /usage goal <goal-id>", elapsed_ms: Date.now() - start };
986
- }
987
- const summary = await this.collectGoalUsage(goalId);
988
- const lines = [
989
- `Usage summary (${scope} scope)`,
990
- `Goal: ${summary.goalId}`,
991
- `Tasks observed: ${summary.taskCount}`,
992
- `Terminal tasks: ${summary.terminalTaskCount}`,
993
- `Total tokens: ${summary.totalTokens}`,
994
- ];
995
- return { success: true, output: lines.join("\n"), elapsed_ms: Date.now() - start };
996
- }
997
- if (scope === "schedule") {
998
- const period = tokens[1] ?? "7d";
999
- try {
1000
- const summary = await this.collectScheduleUsage(period);
1001
- const lines = [
1002
- `Usage summary (schedule, ${summary.period})`,
1003
- `Runs: ${summary.runs}`,
1004
- `Total tokens: ${summary.totalTokens}`,
1005
- ];
1006
- return { success: true, output: lines.join("\n"), elapsed_ms: Date.now() - start };
1007
- }
1008
- catch (err) {
1009
- const message = err instanceof Error ? err.message : String(err);
1010
- return { success: false, output: `Usage: /usage schedule [24h|7d|2w]\nError: ${message}`, elapsed_ms: Date.now() - start };
1011
- }
1012
- }
1013
- return {
1014
- success: false,
1015
- output: "Usage: /usage [session|goal <goal-id>|daemon <goal-id>|schedule [24h|7d|2w]]",
1016
- elapsed_ms: Date.now() - start,
1017
- };
1018
- }
1019
- deterministicChatSummary(messages) {
1020
- const lines = messages.map((message) => `${message.role}: ${message.content.replace(/\s+/g, " ").trim()}`);
1021
- return lines.join("\n").slice(0, 4_000);
1022
- }
1023
- async summarizeChatForCompaction(messages, existingSummary) {
1024
- const content = [
1025
- existingSummary ? `Previous summary:\n${existingSummary}` : "",
1026
- `Messages to summarize:\n${messages.map((message) => `${message.role}: ${message.content}`).join("\n")}`,
1027
- ].filter(Boolean).join("\n\n");
1028
- if (this.deps.llmClient) {
1029
- try {
1030
- const response = await this.deps.llmClient.sendMessage([
1031
- { role: "user", content: `Summarize this chat history for later continuation. Preserve decisions, open tasks, constraints, and user preferences. Keep it concise.\n\n${content}` },
1032
- ], { max_tokens: 700, model_tier: "light" });
1033
- if (response.content.trim())
1034
- return { summary: response.content.trim(), usedLlm: true };
1035
- }
1036
- catch {
1037
- // Fall back to deterministic summary below.
1038
- }
1039
- }
1040
- const fallback = [
1041
- existingSummary ? `Previous summary:\n${existingSummary}` : "",
1042
- "Extractive summary:",
1043
- this.deterministicChatSummary(messages),
1044
- ].filter(Boolean).join("\n\n");
1045
- return { summary: fallback, usedLlm: false };
1046
- }
1047
- async handleCompact(start) {
1048
- if (!this.history) {
1049
- return { success: false, output: "No active chat session to compact.", elapsed_ms: Date.now() - start };
1050
- }
1051
- const session = this.history.getSessionData();
1052
- if (session.messages.length <= 4) {
1053
- return { success: true, output: "Chat history is already compact. No messages were removed.", elapsed_ms: Date.now() - start };
1054
- }
1055
- const olderMessages = session.messages.slice(0, -4);
1056
- const { summary, usedLlm } = await this.summarizeChatForCompaction(olderMessages, session.compactionSummary);
1057
- const { before, after } = await this.history.compact(summary, 4);
1058
- const method = usedLlm ? "LLM summary" : "deterministic summary";
1059
- return {
1060
- success: true,
1061
- output: `Compacted chat history with ${method}. Persisted ${before} message(s) down to ${after}; the latest user/assistant turns were kept.`,
1062
- elapsed_ms: Date.now() - start,
1063
- };
1064
- }
1065
- formatRoute(route) {
1066
- if (!route)
1067
- return "none selected yet";
1068
- const details = [
1069
- `lane=${route.lane}`,
1070
- `kind=${route.kind}`,
1071
- `reason=${route.reason}`,
1072
- ];
1073
- if (route.kind === "direct_answer") {
1074
- details.push(`model_tier=${route.modelTier}`, `max_tokens=${route.maxTokens}`);
1075
- }
1076
- if (route.kind === "runtime_control") {
1077
- details.push(`intent=${route.intent.kind}`);
1078
- }
1079
- return details.join(", ");
1080
- }
1081
- async handleContext(start, cwdOverride) {
1082
- const cwd = this.sessionCwd ?? (cwdOverride ? resolveGitRoot(cwdOverride) : process.cwd());
1083
- const session = this.history?.getSessionData() ?? null;
1084
- const messages = session?.messages ?? [];
1085
- const policy = await this.getSessionExecutionPolicy();
1086
- const recentMessages = messages.slice(-6);
1087
- const userTurns = messages.filter((message) => message.role === "user").length;
1088
- const assistantTurns = messages.filter((message) => message.role === "assistant").length;
1089
- const compactionSummary = session?.compactionSummary?.trim() ?? "";
1090
- const agentLoopPath = this.nativeAgentLoopStatePath ?? session?.agentLoopStatePath ?? null;
1091
- const replyTarget = this.runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget ?? null;
1092
- const routeCapabilities = this.getRouteCapabilities();
1093
- const replyTargetParts = replyTarget
1094
- ? [replyTarget.surface, replyTarget.platform, replyTarget.conversation_id].filter(Boolean)
1095
- : [];
1096
- const contextLines = [
1097
- "Working context",
1098
- "",
1099
- "Session",
1100
- `- session_id: ${this.history?.getSessionId() ?? "none"}`,
1101
- `- cwd: ${cwd}`,
1102
- `- messages: ${messages.length} (${userTurns} user, ${assistantTurns} assistant)`,
1103
- `- recent_turns_retained: ${recentMessages.length}`,
1104
- `- compaction_summary: ${compactionSummary ? "present" : "none"}`,
1105
- `- agentloop_state_path: ${agentLoopPath ?? "none"}`,
1106
- "",
1107
- "Turn context",
1108
- `- last_selected_route: ${this.formatRoute(this.lastSelectedRoute)}`,
1109
- `- reply_target: ${replyTargetParts.length > 0 ? replyTargetParts.join(":") : "none"}`,
1110
- `- route_capabilities: light_llm=${routeCapabilities.hasLightweightLlm}, agent_loop=${routeCapabilities.hasAgentLoop}, tool_loop=${routeCapabilities.hasToolLoop}, runtime_control=${routeCapabilities.hasRuntimeControlService}, daemon_tend=${routeCapabilities.hasDaemonTend}`,
1111
- "",
1112
- "Working assumptions",
1113
- "- this view exposes operational context, not hidden reasoning",
1114
- "- last_selected_route describes the most recent non-command turn in this ChatRunner",
1115
- "- future turns may select a different route based on the next input",
1116
- "",
1117
- "Active constraints",
1118
- ...summarizeExecutionPolicy(policy).split("\n").map((line) => `- ${line}`),
1119
- "",
1120
- "Included context",
1121
- "- current session cwd and execution policy because they constrain tool and route behavior",
1122
- `- ${recentMessages.length} latest persisted message(s)`,
1123
- `- ${compactionSummary ? "compacted older chat summary because older turns were summarized" : "no compacted older chat summary because none is stored"}`,
1124
- `- ${agentLoopPath ? "native agent-loop resume path because this session can persist agent-loop state" : "no native agent-loop resume path because none is active"}`,
1125
- "",
1126
- "Not included",
1127
- "- hidden reasoning or private model chain-of-thought",
1128
- "- raw state files unless a command explicitly reads them",
1129
- "- older chat turns beyond the retained window unless compacted into the session summary",
1130
- ];
1131
- return {
1132
- success: true,
1133
- output: contextLines.join("\n"),
1134
- elapsed_ms: Date.now() - start,
1135
- };
1136
- }
1137
- async handleCommand(input, cwd) {
1138
- const trimmed = input.trim();
1139
- if (!trimmed.startsWith("/"))
1140
- return null;
1141
- const cmd = trimmed.toLowerCase().split(/\s+/)[0];
1142
- const start = Date.now();
1143
- if (cmd === "/help") {
1144
- return { success: true, output: COMMAND_HELP, elapsed_ms: Date.now() - start };
1145
- }
1146
- if (cmd === "/clear") {
1147
- await this.history?.clear();
1148
- return { success: true, output: "Conversation history cleared.", elapsed_ms: Date.now() - start };
1149
- }
1150
- if (cmd === "/sessions") {
1151
- const registry = createRuntimeSessionRegistry({ stateManager: this.deps.stateManager });
1152
- const snapshot = await registry.snapshot();
1153
- return { success: true, output: this.formatRuntimeSessionsList(snapshot), elapsed_ms: Date.now() - start };
1154
- }
1155
- if (cmd === "/history") {
1156
- const catalog = new ChatSessionCatalog(this.deps.stateManager);
1157
- const selector = trimmed.slice("/history".length).trim();
1158
- const session = selector
1159
- ? await catalog.loadSessionBySelector(selector)
1160
- : this.history
1161
- ? await catalog.loadSession(this.history.getSessionId())
1162
- : null;
1163
- if (!session) {
1164
- return { success: false, output: "No chat session history found.", elapsed_ms: Date.now() - start };
1165
- }
1166
- return { success: true, output: this.formatHistory(session), elapsed_ms: Date.now() - start };
1167
- }
1168
- if (cmd === "/title") {
1169
- const title = trimmed.slice("/title".length).trim();
1170
- if (!title) {
1171
- return { success: false, output: "Usage: /title <title>", elapsed_ms: Date.now() - start };
1172
- }
1173
- if (!this.history) {
1174
- return { success: false, output: "No active chat session to rename.", elapsed_ms: Date.now() - start };
1175
- }
1176
- const catalog = new ChatSessionCatalog(this.deps.stateManager);
1177
- this.history.setTitle(title);
1178
- await this.history.persist();
1179
- await catalog.renameSession(this.history.getSessionId(), title);
1180
- return { success: true, output: `Renamed chat session to "${title}".`, elapsed_ms: Date.now() - start };
1181
- }
1182
- if (cmd === "/cleanup") {
1183
- const catalog = new ChatSessionCatalog(this.deps.stateManager);
1184
- const dryRun = trimmed.includes("--dry-run");
1185
- const report = await catalog.cleanupSessions({
1186
- dryRun,
1187
- activeSessionId: this.history?.getSessionId(),
1188
- });
1189
- const verb = dryRun ? "would remove" : "removed";
1190
- return {
1191
- success: true,
1192
- output: `Chat session cleanup ${verb} ${report.removedSessionIds.length} session(s).`,
1193
- elapsed_ms: Date.now() - start,
1194
- };
1195
- }
1196
- if (cmd === "/compact") {
1197
- return this.handleCompact(start);
1198
- }
1199
- if (cmd === "/status") {
1200
- return this.handleStatus(trimmed.slice("/status".length).trim(), start);
1201
- }
1202
- if (cmd === "/goals") {
1203
- return this.handleGoals(start);
1204
- }
1205
- if (cmd === "/tasks") {
1206
- return this.handleTasks(trimmed.slice("/tasks".length).trim(), start);
1207
- }
1208
- if (cmd === "/task") {
1209
- return this.handleTask(trimmed.slice("/task".length).trim(), start);
1210
- }
1211
- if (cmd === "/config") {
1212
- return this.handleConfig(start);
1213
- }
1214
- if (cmd === "/model") {
1215
- return this.handleModel(start);
1216
- }
1217
- if (cmd === "/permissions") {
1218
- return this.handlePermissions(trimmed.slice("/permissions".length).trim(), start);
1219
- }
1220
- if (cmd === "/plugins") {
1221
- return this.handlePlugins(start);
1222
- }
1223
- if (cmd === "/usage") {
1224
- return this.handleUsage(trimmed.slice("/usage".length).trim(), start);
1225
- }
1226
- if (cmd === "/context" || cmd === "/working-memory") {
1227
- return this.handleContext(start, cwd);
1228
- }
1229
- if (cmd === "/review") {
1230
- return this.handleReview(start);
1231
- }
1232
- if (cmd === "/fork") {
1233
- return this.handleFork(trimmed.slice("/fork".length).trim(), start);
1234
- }
1235
- if (cmd === "/undo") {
1236
- return this.handleUndo(start);
1237
- }
1238
- if (cmd === "/retry") {
1239
- return {
1240
- success: false,
1241
- output: [
1242
- "/retry is not supported yet.",
1243
- "",
1244
- formatFailureRecovery({
1245
- kind: "runtime_interruption",
1246
- label: "Retry unavailable",
1247
- summary: "PulSeed does not yet have a safe replay contract for the previous turn.",
1248
- nextActions: [
1249
- "Use /review to inspect any current diff before continuing.",
1250
- "Use /resume when PulSeed reports resumable agent-loop state.",
1251
- "Ask for the exact next step to rerun instead of replaying the full turn.",
1252
- ],
1253
- }),
1254
- ].join("\n"),
1255
- elapsed_ms: Date.now() - start,
1256
- };
1257
- }
1258
- if (cmd === "/exit") {
1259
- return { success: true, output: "Exiting chat mode.", elapsed_ms: Date.now() - start };
1260
- }
1261
- if (cmd === "/track") {
1262
- return this.handleTrack(start);
1263
- }
1264
- if (cmd === "/tend") {
1265
- const args = trimmed.slice("/tend".length).trim();
1266
- return this.handleTend(args, start);
1267
- }
1268
- // Check if this is a confirmation response for a pending /tend
1269
- if (this.pendingTend !== null) {
1270
- return this.handleTendConfirmation(trimmed, start);
1271
- }
1272
- return {
1273
- success: false,
1274
- output: `Unknown command: ${input.trim()}. Type /help for available commands.`,
1275
- elapsed_ms: Date.now() - start,
1276
- };
1277
- }
1278
- async handleTrack(start) {
1279
- if (!this.deps.escalationHandler) {
1280
- return {
1281
- success: false,
1282
- output: "Escalation not available — missing LLM configuration",
1283
- elapsed_ms: Date.now() - start,
1284
- };
1285
- }
1286
- if (!this.history || this.history.getMessages().length === 0) {
1287
- return {
1288
- success: false,
1289
- output: "No conversation to escalate. Chat first, then /track.",
1290
- elapsed_ms: Date.now() - start,
1291
- };
1292
- }
1293
- try {
1294
- const result = await this.deps.escalationHandler.escalateToGoal(this.history);
1295
- return {
1296
- success: true,
1297
- output: `Goal created: ${result.title} (ID: ${result.goalId})\nRun: pulseed run --goal ${result.goalId} --yes`,
1298
- elapsed_ms: Date.now() - start,
1299
- };
1300
- }
1301
- catch (err) {
1302
- const message = err instanceof Error ? err.message : String(err);
1303
- return {
1304
- success: false,
1305
- output: `Escalation failed: ${message}`,
1306
- elapsed_ms: Date.now() - start,
1307
- };
1308
- }
1309
- }
1310
- async handlePermissions(args, start) {
1311
- const policy = await this.getSessionExecutionPolicy();
1312
- if (!args) {
1313
- return {
1314
- success: true,
1315
- output: summarizeExecutionPolicy(policy),
1316
- elapsed_ms: Date.now() - start,
1317
- };
1318
- }
1319
- const tokens = args.toLowerCase().split(/\s+/).filter(Boolean);
1320
- let nextPolicy = policy;
1321
- for (let index = 0; index < tokens.length; index++) {
1322
- const token = tokens[index];
1323
- if (token === "read-only" || token === "readonly" || token === "read_only") {
1324
- nextPolicy = withExecutionPolicyOverrides(nextPolicy, { sandboxMode: "read_only" });
1325
- continue;
1326
- }
1327
- if (token === "workspace-write" || token === "workspace_write") {
1328
- nextPolicy = withExecutionPolicyOverrides(nextPolicy, { sandboxMode: "workspace_write" });
1329
- continue;
1330
- }
1331
- if (token === "full-access" || token === "danger-full-access" || token === "danger_full_access") {
1332
- nextPolicy = withExecutionPolicyOverrides(nextPolicy, { sandboxMode: "danger_full_access" });
1333
- continue;
1334
- }
1335
- if (token === "network" && tokens[index + 1]) {
1336
- nextPolicy = withExecutionPolicyOverrides(nextPolicy, { networkAccess: tokens[index + 1] === "on" });
1337
- index += 1;
1338
- continue;
1339
- }
1340
- if (token === "approval" && tokens[index + 1]) {
1341
- const approvalPolicy = tokens[index + 1];
1342
- if (approvalPolicy === "never" || approvalPolicy === "on_request" || approvalPolicy === "untrusted") {
1343
- nextPolicy = withExecutionPolicyOverrides(nextPolicy, { approvalPolicy });
1344
- index += 1;
1345
- continue;
1346
- }
1347
- }
1348
- return {
1349
- success: false,
1350
- output: "Usage: /permissions [read-only|workspace-write|full-access] [network on|off] [approval on_request|never|untrusted]",
1351
- elapsed_ms: Date.now() - start,
1352
- };
1353
- }
1354
- this.sessionExecutionPolicy = nextPolicy;
1355
- return {
1356
- success: true,
1357
- output: summarizeExecutionPolicy(nextPolicy),
1358
- elapsed_ms: Date.now() - start,
1359
- };
1360
- }
1361
- async handleReview(start) {
1362
- const cwd = this.sessionCwd ?? process.cwd();
1363
- const diffStat = await checkGitChanges(cwd);
1364
- const reviewPolicy = withExecutionPolicyOverrides(await this.getSessionExecutionPolicy(), {
1365
- sandboxMode: "read_only",
1366
- approvalPolicy: "never",
1367
- });
1368
- if (this.deps.reviewAgentLoopRunner) {
1369
- const review = await this.deps.reviewAgentLoopRunner.execute({
1370
- cwd,
1371
- diffStat,
1372
- executionPolicy: reviewPolicy,
1373
- });
1374
- return { success: review.success, output: review.output, elapsed_ms: Date.now() - start };
1375
- }
1376
- const output = [
1377
- "Review summary",
1378
- diffStat ? diffStat : "No uncommitted changes detected.",
1379
- "",
1380
- "Execution policy",
1381
- summarizeExecutionPolicy(reviewPolicy),
1382
- ].join("\n");
1383
- return { success: true, output, elapsed_ms: Date.now() - start };
1384
- }
1385
- async handleFork(title, start) {
1386
- const cwd = this.sessionCwd ?? process.cwd();
1387
- const sessionId = crypto.randomUUID();
1388
- const baseSession = this.history?.getSessionData() ?? {
1389
- id: sessionId,
1390
- cwd,
1391
- createdAt: new Date().toISOString(),
1392
- updatedAt: new Date().toISOString(),
1393
- messages: [],
1394
- };
1395
- const now = new Date().toISOString();
1396
- const forkedSession = {
1397
- ...baseSession,
1398
- id: sessionId,
1399
- createdAt: now,
1400
- updatedAt: now,
1401
- title: title || (baseSession.title ? `${baseSession.title} (fork)` : "Forked session"),
1402
- };
1403
- this.history = ChatHistory.fromSession(this.deps.stateManager, forkedSession);
1404
- this.sessionCwd = cwd;
1405
- this.sessionActive = true;
1406
- this.nativeAgentLoopStatePath = `chat/agentloop/${sessionId}.state.json`;
1407
- this.history.setAgentLoopStatePath(this.nativeAgentLoopStatePath);
1408
- await this.history.persist();
1409
- return {
1410
- success: true,
1411
- output: `Forked chat session as ${sessionId}.`,
1412
- elapsed_ms: Date.now() - start,
1413
- };
1414
- }
1415
- async handleUndo(start) {
1416
- if (!this.history) {
1417
- return { success: false, output: "No active chat session to undo.", elapsed_ms: Date.now() - start };
1418
- }
1419
- const removed = await this.history.removeLastTurn();
1420
- if (removed === 0) {
1421
- return { success: false, output: "No chat turn to undo.", elapsed_ms: Date.now() - start };
1422
- }
1423
- return {
1424
- success: true,
1425
- output: `Removed ${removed} message(s) from chat history. File changes were not reverted.`,
1426
- elapsed_ms: Date.now() - start,
1427
- };
1428
- }
1429
- async handleTend(args, start) {
1430
- if (!this.deps.llmClient) {
1431
- return {
1432
- success: false,
1433
- output: "Tend not available — missing LLM configuration",
1434
- elapsed_ms: Date.now() - start,
1435
- };
1436
- }
1437
- if (!this.deps.goalNegotiator) {
1438
- return {
1439
- success: false,
1440
- output: "Tend not available — missing goal negotiator",
1441
- elapsed_ms: Date.now() - start,
1442
- };
1443
- }
1444
- if (!this.deps.daemonClient) {
1445
- return {
1446
- success: false,
1447
- output: "Tend not available — daemon client not configured. Start the daemon with 'pulseed daemon start' first.",
1448
- elapsed_ms: Date.now() - start,
1449
- };
1450
- }
1451
- const history = this.history?.getMessages() ?? [];
1452
- const tendDeps = {
1453
- llmClient: this.deps.llmClient,
1454
- goalNegotiator: this.deps.goalNegotiator,
1455
- daemonClient: this.deps.daemonClient,
1456
- stateManager: this.deps.stateManager,
1457
- chatHistory: history,
1458
- sessionId: this.history?.getSessionId() ?? null,
1459
- workspace: this.sessionCwd ?? process.cwd(),
1460
- replyTarget: this.runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget ?? null,
1461
- };
1462
- const tendCommand = new TendCommand();
1463
- const result = await tendCommand.execute(args, tendDeps);
1464
- if (result.needsConfirmation && result.goalId) {
1465
- this.pendingTend = { goalId: result.goalId, maxIterations: result.maxIterations };
1466
- return {
1467
- success: true,
1468
- output: result.confirmation ?? result.message,
1469
- elapsed_ms: Date.now() - start,
1470
- };
1471
- }
1472
- return {
1473
- success: result.success,
1474
- output: result.message,
1475
- elapsed_ms: Date.now() - start,
1476
- };
1477
- }
1478
- async handleTendConfirmation(input, start) {
1479
- const pending = this.pendingTend;
1480
- this.pendingTend = null;
1481
- const normalized = input.trim().toLowerCase();
1482
- const confirmed = normalized === "" || normalized === "y" || normalized === "yes";
1483
- if (!confirmed) {
1484
- // Bug 2: treat any non-y/yes/empty/n/no input as cancellation too
1485
- return {
1486
- success: true,
1487
- output: "Tend cancelled. Continue chatting to refine your goal, then try /tend again.",
1488
- elapsed_ms: Date.now() - start,
1489
- };
1490
- }
1491
- if (!this.deps.daemonClient) {
1492
- return {
1493
- success: false,
1494
- output: "Daemon client not available.",
1495
- elapsed_ms: Date.now() - start,
1496
- };
1497
- }
1498
- const { goalId, maxIterations } = pending;
1499
- let subscriber = null;
1500
- if (this.deps.daemonBaseUrl && !this.activeSubscribers.has(goalId)) {
1501
- subscriber = new EventSubscriber(this.deps.daemonBaseUrl, goalId, "normal");
1502
- this.activeSubscribers.set(goalId, subscriber);
1503
- subscriber.on("notification", (notification) => {
1504
- const n = notification;
1505
- this.deps.onNotification?.(n.message);
1506
- this.onNotification?.(n.message);
1507
- });
1508
- subscriber.on("chat_event", (event) => {
1509
- this.emitEvent(event);
1510
- });
1511
- try {
1512
- await subscriber.subscribeReady();
1513
- }
1514
- catch (err) {
1515
- subscriber.unsubscribe();
1516
- this.activeSubscribers.delete(goalId);
1517
- const msg = err instanceof Error ? err.message : String(err);
1518
- return {
1519
- success: false,
1520
- output: `Daemon event stream unavailable: ${msg}. Goal was not started.`,
1521
- elapsed_ms: Date.now() - start,
1522
- };
1523
- }
1524
- }
1525
- try {
1526
- const tendDeps = {
1527
- llmClient: this.deps.llmClient,
1528
- goalNegotiator: this.deps.goalNegotiator,
1529
- daemonClient: this.deps.daemonClient,
1530
- stateManager: this.deps.stateManager,
1531
- chatHistory: this.history?.getMessages() ?? [],
1532
- sessionId: this.history?.getSessionId() ?? null,
1533
- workspace: this.sessionCwd ?? process.cwd(),
1534
- replyTarget: this.runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget ?? null,
1535
- };
1536
- const result = await new TendCommand().startAcceptedGoal(goalId, maxIterations, tendDeps);
1537
- if (!result.success) {
1538
- if (subscriber) {
1539
- subscriber.unsubscribe();
1540
- this.activeSubscribers.delete(goalId);
1541
- }
1542
- return {
1543
- success: false,
1544
- output: result.message,
1545
- elapsed_ms: Date.now() - start,
1546
- };
1547
- }
1548
- const shortId = goalId.length > 12 ? goalId.slice(0, 12) : goalId;
1549
- return {
1550
- success: true,
1551
- output: `[tend] ${shortId}: Started — daemon is now tending your goal${maxIterations !== undefined ? ` (max ${maxIterations} iterations)` : ""}.\nBackground run: ${result.backgroundRunId}\nRun 'pulseed status' to check progress.`,
1552
- elapsed_ms: Date.now() - start,
1553
- };
1554
- }
1555
- catch (err) {
1556
- if (subscriber) {
1557
- subscriber.unsubscribe();
1558
- this.activeSubscribers.delete(goalId);
1559
- }
1560
- const msg = err instanceof Error ? err.message : String(err);
1561
- return {
1562
- success: false,
1563
- output: `Daemon unavailable: ${msg}. Start the daemon with 'pulseed daemon start' first.`,
1564
- elapsed_ms: Date.now() - start,
1565
- };
1566
- }
1567
- }
1568
- /**
1569
- * Execute a single chat turn.
1570
- *
1571
- * Flow:
1572
- * 1. Intercept slash commands before adapter dispatch
1573
- * 2. Resolve git root → create ChatHistory
1574
- * 3. Build chat context and assemble prompt
1575
- * 4. Persist user message BEFORE calling adapter (crash-safe)
1576
- * 5. Execute via adapter
1577
- * 6. Verify changes (git diff + tests); retry up to MAX_VERIFY_RETRIES if tests fail
1578
- * 7. Persist assistant response only after the final assistant text is complete
1579
- */
1580
- async execute(input, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
1581
- const eventContext = this.createEventContext();
1582
- const activeTurn = this.beginActiveTurn(eventContext, resolveGitRoot(cwd));
1583
- const resumeCommand = this.parseResumeCommand(input);
1584
- const resumeOnly = resumeCommand !== null;
1585
- const runtimeControlContext = options.runtimeControlContext ?? this.runtimeControlContext;
1586
- const executionGoalId = options.goalId ?? this.deps.goalId;
1587
- // Intercept commands before any adapter call
1588
- const commandResult = resumeOnly ? null : await this.handleCommand(input, cwd);
1589
- if (commandResult !== null) {
1590
- if (commandResult.output) {
1591
- this.emitEvent({
1592
- type: "assistant_final",
1593
- text: commandResult.output,
1594
- persisted: false,
1595
- ...this.eventBase(eventContext),
1596
- });
1597
- }
1598
- this.emitLifecycleEndEvent(commandResult.success ? "completed" : "error", commandResult.elapsed_ms, eventContext, false);
1599
- return commandResult;
1600
- }
1601
- // Intercept plain Y/n responses (and any other input) when a /tend confirmation is pending
1602
- if (this.pendingTend !== null && !resumeOnly) {
1603
- const confirmationResult = await this.handleTendConfirmation(input.trim(), Date.now());
1604
- if (confirmationResult.output) {
1605
- this.emitEvent({
1606
- type: "assistant_final",
1607
- text: confirmationResult.output,
1608
- persisted: false,
1609
- ...this.eventBase(eventContext),
1610
- });
1611
- }
1612
- this.emitLifecycleEndEvent(confirmationResult.success ? "completed" : "error", confirmationResult.elapsed_ms, eventContext, false);
1613
- return confirmationResult;
1614
- }
1615
- if (resumeOnly && resumeCommand.selector) {
1616
- try {
1617
- const selectorResolution = await this.resolveChatResumeSelector(resumeCommand.selector);
1618
- if (selectorResolution.nonResumableMessage) {
1619
- const elapsed_ms = 0;
1620
- const output = selectorResolution.nonResumableMessage;
1621
- this.emitEvent({
1622
- type: "assistant_final",
1623
- text: output,
1624
- persisted: false,
1625
- ...this.eventBase(eventContext),
1626
- });
1627
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1628
- return { success: false, output, elapsed_ms };
1629
- }
1630
- const catalog = new ChatSessionCatalog(this.deps.stateManager);
1631
- const session = await catalog.loadSessionBySelector(selectorResolution.chatSelector);
1632
- if (!session) {
1633
- const elapsed_ms = 0;
1634
- const output = `No chat session matched selector "${selectorResolution.chatSelector}".`;
1635
- this.emitEvent({
1636
- type: "assistant_final",
1637
- text: output,
1638
- persisted: false,
1639
- ...this.eventBase(eventContext),
1640
- });
1641
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1642
- return { success: false, output, elapsed_ms };
1643
- }
1644
- this.startSessionFromLoadedSession(session);
1645
- }
1646
- catch (err) {
1647
- const elapsed_ms = 0;
1648
- const output = err instanceof ChatSessionSelectorError ? err.message : `Failed to load chat session: ${err instanceof Error ? err.message : String(err)}`;
1649
- this.emitEvent({
1650
- type: "assistant_final",
1651
- text: output,
1652
- persisted: false,
1653
- ...this.eventBase(eventContext),
1654
- });
1655
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1656
- return { success: false, output, elapsed_ms };
1657
- }
1658
- }
1659
- // Reuse session (interactive mode) or create a fresh one per call (1-shot mode)
1660
- if (!this.sessionActive) {
1661
- const gitRoot = resolveGitRoot(cwd);
1662
- const sessionId = crypto.randomUUID();
1663
- this.history = new ChatHistory(this.deps.stateManager, sessionId, gitRoot);
1664
- this.nativeAgentLoopStatePath = `chat/agentloop/${sessionId}.state.json`;
1665
- this.history.setAgentLoopStatePath(this.nativeAgentLoopStatePath);
1666
- }
1667
- const executionCwd = this.sessionCwd ?? cwd;
1668
- const gitRoot = this.sessionCwd ?? resolveGitRoot(cwd);
1669
- activeTurn.cwd = gitRoot;
1670
- // history is always assigned by this point (either by startSession or the block above)
1671
- const history = this.history;
1672
- this.emitEvent({
1673
- type: "lifecycle_start",
1674
- input,
1675
- ...this.eventBase(eventContext),
1676
- });
1677
- // Persist-before-execute: user message written to disk before model or adapter execution.
1678
- if (!resumeOnly) {
1679
- await history.appendUserMessage(input);
1680
- }
1681
- // Build static grounding once per session; dynamic context is rebuilt each turn.
1682
- if (this.cachedStaticSystemPrompt === null) {
1683
- try {
1684
- this.cachedStaticSystemPrompt = buildStaticSystemPrompt(this.providerConfigBaseDir());
1685
- }
1686
- catch {
1687
- this.cachedStaticSystemPrompt = "";
1688
- }
1689
- }
1690
- // Build conversation history from prior turns (last 10), including any manual compaction summary.
1691
- const messages = history.getMessages();
1692
- const compactionSummary = history.getSessionData().compactionSummary;
1693
- const priorTurns = resumeOnly ? messages.slice(-10) : messages.slice(0, -1).slice(-10);
1694
- let historyBlock = "";
1695
- const historySections = [];
1696
- if (compactionSummary) {
1697
- historySections.push(`Compacted previous conversation summary:\n${compactionSummary}`);
1698
- }
1699
- if (priorTurns.length > 0) {
1700
- const lines = priorTurns.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
1701
- historySections.push(`Previous conversation:\n${lines}`);
1702
- }
1703
- if (historySections.length > 0) {
1704
- historyBlock = `${historySections.join("\n\n")}\n\nCurrent message:\n`;
1705
- }
1706
- const selectedRoute = resumeOnly
1707
- ? null
1708
- : (options.selectedRoute ?? this.resolveRouteFromInput(input, runtimeControlContext));
1709
- this.lastSelectedRoute = selectedRoute;
1710
- const directPrompt = historyBlock ? `${historyBlock}${input}` : input;
1711
- if (!resumeOnly) {
1712
- this.emitIntent(input, selectedRoute, eventContext);
1713
- }
1714
- else if (resumeOnly) {
1715
- this.emitIntent(input, null, eventContext);
1716
- }
1717
- const start = Date.now();
1718
- const assistantBuffer = { text: "" };
1719
- const turnUsage = this.zeroUsageCounter();
1720
- const identityResponse = resumeOnly ? null : resolveSelfIdentityResponse(input, this.providerConfigBaseDir());
1721
- if (identityResponse !== null) {
1722
- const elapsed_ms = Date.now() - start;
1723
- await history.appendAssistantMessage(identityResponse);
1724
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
1725
- this.emitEvent({
1726
- type: "assistant_final",
1727
- text: identityResponse,
1728
- persisted: true,
1729
- ...this.eventBase(eventContext),
1730
- });
1731
- this.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
1732
- return {
1733
- success: true,
1734
- output: identityResponse,
1735
- elapsed_ms,
1736
- };
1737
- }
1738
- if (selectedRoute?.kind === "runtime_control") {
1739
- this.emitCheckpoint("Runtime control selected", `${selectedRoute.intent.kind} request recognized.`, eventContext, "route");
1740
- const runtimeControlResult = await this.executeRuntimeControlRoute(selectedRoute, runtimeControlContext, executionCwd, start);
1741
- if (runtimeControlResult.success) {
1742
- await history.appendAssistantMessage(runtimeControlResult.output);
1743
- this.emitCheckpoint("Runtime control completed", "The runtime-control operation produced a result.", eventContext, "complete");
1744
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
1745
- this.emitEvent({
1746
- type: "assistant_final",
1747
- text: runtimeControlResult.output,
1748
- persisted: true,
1749
- ...this.eventBase(eventContext),
1750
- });
1751
- this.emitLifecycleEndEvent("completed", runtimeControlResult.elapsed_ms, eventContext, true);
1752
- }
1753
- else {
1754
- runtimeControlResult.output = this.emitLifecycleErrorEvent(runtimeControlResult.output, assistantBuffer.text, eventContext);
1755
- this.emitLifecycleEndEvent("error", runtimeControlResult.elapsed_ms, eventContext, false);
1756
- }
1757
- return runtimeControlResult;
1758
- }
1759
- if (selectedRoute?.kind === "daemon_tend") {
1760
- this.emitCheckpoint("Durable goal selected", "This long-running request is being prepared for daemon-backed CoreLoop execution.", eventContext, "route");
1761
- const tendResult = await this.handleTend("", start);
1762
- if (tendResult.success) {
1763
- await history.appendAssistantMessage(tendResult.output);
1764
- this.emitCheckpoint("Durable goal prepared", "The daemon-backed goal confirmation is ready.", eventContext, "complete");
1765
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
1766
- this.emitEvent({
1767
- type: "assistant_final",
1768
- text: tendResult.output,
1769
- persisted: true,
1770
- ...this.eventBase(eventContext),
1771
- });
1772
- this.emitLifecycleEndEvent("completed", tendResult.elapsed_ms, eventContext, true);
1773
- }
1774
- else {
1775
- tendResult.output = this.emitLifecycleErrorEvent(tendResult.output, assistantBuffer.text, eventContext);
1776
- this.emitLifecycleEndEvent("error", tendResult.elapsed_ms, eventContext, false);
1777
- }
1778
- return tendResult;
1779
- }
1780
- if (selectedRoute?.kind === "direct_answer") {
1781
- try {
1782
- this.emitActivity("lifecycle", "Calling model...", eventContext, "lifecycle:model");
1783
- const directResponse = await this.sendLLMMessage(this.deps.llmClient, [{ role: "user", content: directPrompt }], {
1784
- ...(this.cachedStaticSystemPrompt ? { system: this.cachedStaticSystemPrompt } : {}),
1785
- model_tier: selectedRoute.modelTier,
1786
- max_tokens: selectedRoute.maxTokens,
1787
- }, assistantBuffer, eventContext);
1788
- this.addUsageCounter(turnUsage, this.usageFromLLMResponse(directResponse));
1789
- const elapsed_ms = Date.now() - start;
1790
- const output = assistantBuffer.text || directResponse.content || "(no response)";
1791
- if (this.hasUsage(turnUsage)) {
1792
- history.recordUsage("execution", turnUsage);
1793
- }
1794
- const diffArtifact = await collectGitDiffArtifact(gitRoot);
1795
- if (diffArtifact) {
1796
- this.emitDiffArtifact(diffArtifact, eventContext);
1797
- }
1798
- await history.appendAssistantMessage(output);
1799
- this.emitCheckpoint("Response ready", "The direct answer has been persisted for this turn.", eventContext, "complete");
1800
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
1801
- this.emitEvent({
1802
- type: "assistant_final",
1803
- text: output,
1804
- persisted: true,
1805
- ...this.eventBase(eventContext),
1806
- });
1807
- this.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
1808
- return {
1809
- success: true,
1810
- output,
1811
- elapsed_ms,
1812
- diagnostics: {
1813
- route: "direct",
1814
- reason: selectedRoute.reason,
1815
- modelTier: selectedRoute.modelTier,
1816
- maxTokens: selectedRoute.maxTokens,
1817
- },
1818
- };
1819
- }
1820
- catch (err) {
1821
- const message = err instanceof Error ? err.message : String(err);
1822
- const output = this.emitLifecycleErrorEvent(message, assistantBuffer.text, eventContext);
1823
- this.emitLifecycleEndEvent("error", Date.now() - start, eventContext, false);
1824
- return {
1825
- success: false,
1826
- output,
1827
- elapsed_ms: Date.now() - start,
1828
- diagnostics: {
1829
- route: "direct",
1830
- reason: selectedRoute.reason,
1831
- modelTier: selectedRoute.modelTier,
1832
- maxTokens: selectedRoute.maxTokens,
1833
- },
1834
- };
1835
- }
1836
- }
1837
- const usesNativeAgentLoop = resumeOnly || selectedRoute?.kind === "agent_loop";
1838
- const groundingWorkspaceContext = !resumeOnly && usesNativeAgentLoop
1839
- ? await buildChatContext(input, executionCwd)
1840
- : undefined;
1841
- let systemPrompt = this.cachedStaticSystemPrompt ?? "";
1842
- if (!resumeOnly) {
1843
- try {
1844
- this.emitActivity("lifecycle", "Preparing context...", eventContext, "lifecycle:context");
1845
- if (usesNativeAgentLoop) {
1846
- systemPrompt = await buildChatAgentLoopSystemPrompt({
1847
- stateManager: this.deps.stateManager,
1848
- pluginLoader: this.deps.pluginLoader,
1849
- workspaceRoot: executionCwd,
1850
- goalId: executionGoalId,
1851
- userMessage: input,
1852
- trustProjectInstructions: this.sessionExecutionPolicy?.trustProjectInstructions ?? true,
1853
- workspaceContext: groundingWorkspaceContext,
1854
- });
1855
- }
1856
- else {
1857
- const groundingBundle = await this.groundingGateway.build({
1858
- surface: "chat",
1859
- purpose: "general_turn",
1860
- workspaceRoot: executionCwd,
1861
- goalId: executionGoalId,
1862
- userMessage: input,
1863
- query: input,
1864
- trustProjectInstructions: this.sessionExecutionPolicy?.trustProjectInstructions ?? true,
1865
- });
1866
- systemPrompt = String(groundingBundle.render("prompt"));
1867
- }
1868
- }
1869
- catch {
1870
- systemPrompt = this.cachedStaticSystemPrompt ?? "";
1871
- }
1872
- this.emitCheckpoint("Context gathered", usesNativeAgentLoop
1873
- ? "Workspace and agent-loop grounding are ready."
1874
- : "Workspace grounding is ready.", eventContext, "context");
1875
- }
1876
- const agentLoopSystemPrompt = [
1877
- systemPrompt,
1878
- compactionSummary ? `## Compacted Chat Summary\n${compactionSummary}` : "",
1879
- ]
1880
- .filter((section) => section && section.trim().length > 0)
1881
- .join("\n\n")
1882
- .trim();
1883
- const context = resumeOnly || usesNativeAgentLoop ? "" : await buildChatContext(input, gitRoot);
1884
- const basePrompt = resumeOnly ? "" : (context ? `${context}\n\n${input}` : input);
1885
- const prompt = historyBlock ? `${historyBlock}${basePrompt}` : basePrompt;
1886
- if (resumeOnly && !this.deps.chatAgentLoopRunner) {
1887
- const elapsed_ms = Date.now() - start;
1888
- const output = this.emitLifecycleErrorEvent("Resume requires the native chat agentloop runtime.", assistantBuffer.text, eventContext);
1889
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1890
- return {
1891
- success: false,
1892
- output,
1893
- elapsed_ms,
1894
- };
1895
- }
1896
- const chatAgentLoopRunner = this.deps.chatAgentLoopRunner;
1897
- if (resumeOnly || selectedRoute?.kind === "agent_loop") {
1898
- try {
1899
- const resumeState = resumeOnly ? await this.loadResumableAgentLoopState() : null;
1900
- if (resumeOnly && !resumeState) {
1901
- const elapsed_ms = Date.now() - start;
1902
- const output = this.emitLifecycleErrorEvent("No resumable native agentloop state found.", assistantBuffer.text, eventContext);
1903
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1904
- return {
1905
- success: false,
1906
- output,
1907
- elapsed_ms,
1908
- };
1909
- }
1910
- this.emitCheckpoint(resumeOnly ? "Session resumed" : "Agent loop started", resumeOnly
1911
- ? "Resumable agent-loop state is loaded."
1912
- : "The agent loop can now inspect, plan, edit, or verify with visible tool activity.", eventContext, "execution");
1913
- this.emitActivity("lifecycle", "Calling model...", eventContext, "lifecycle:model");
1914
- const result = await chatAgentLoopRunner.execute({
1915
- message: basePrompt,
1916
- cwd: executionCwd,
1917
- goalId: executionGoalId,
1918
- history: priorTurns.map((m) => ({
1919
- role: m.role === "assistant" ? "assistant" : "user",
1920
- content: m.content,
1921
- })),
1922
- eventSink: this.createAgentLoopEventSink(eventContext),
1923
- approvalFn: async (request) => {
1924
- if (this.deps.approvalFn) {
1925
- return this.deps.approvalFn(request.reason);
1926
- }
1927
- return false;
1928
- },
1929
- toolCallContext: {
1930
- executionPolicy: await this.getSessionExecutionPolicy(),
1931
- },
1932
- ...(this.nativeAgentLoopStatePath ? { resumeStatePath: this.nativeAgentLoopStatePath } : {}),
1933
- ...(resumeState ? { resumeState } : {}),
1934
- ...(resumeOnly ? { resumeOnly: true } : {}),
1935
- ...(agentLoopSystemPrompt ? { systemPrompt: agentLoopSystemPrompt } : {}),
1936
- abortSignal: activeTurn.abortController.signal,
1937
- });
1938
- const elapsed_ms = Date.now() - start;
1939
- const agentLoopUsage = result.agentLoop?.usage
1940
- ? this.normalizeUsageCounter(result.agentLoop.usage)
1941
- : this.zeroUsageCounter();
1942
- if (this.hasUsage(agentLoopUsage)) {
1943
- history.recordUsage("agentloop", agentLoopUsage);
1944
- }
1945
- if (result.output) {
1946
- this.pushAssistantDelta(result.output, assistantBuffer, eventContext);
1947
- }
1948
- if (result.success) {
1949
- const diffArtifact = await collectGitDiffArtifact(gitRoot);
1950
- if (diffArtifact) {
1951
- this.emitDiffArtifact(diffArtifact, eventContext);
1952
- }
1953
- await history.appendAssistantMessage(result.output);
1954
- this.emitCheckpoint("Response ready", "The agent-loop response has been persisted for this turn.", eventContext, "complete");
1955
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
1956
- this.emitEvent({
1957
- type: "assistant_final",
1958
- text: result.output,
1959
- persisted: true,
1960
- ...this.eventBase(eventContext),
1961
- });
1962
- this.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
1963
- }
1964
- else {
1965
- result.output = this.emitLifecycleErrorEvent(result.output || result.error || "Unknown error", assistantBuffer.text, eventContext);
1966
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
1967
- }
1968
- return {
1969
- success: result.success,
1970
- output: result.output,
1971
- elapsed_ms,
1972
- };
1973
- }
1974
- catch (err) {
1975
- const message = err instanceof Error ? err.message : String(err);
1976
- const output = this.emitLifecycleErrorEvent(message, assistantBuffer.text, eventContext);
1977
- this.emitLifecycleEndEvent("error", Date.now() - start, eventContext, false);
1978
- return {
1979
- success: false,
1980
- output,
1981
- elapsed_ms: Date.now() - start,
1982
- };
1983
- }
1984
- }
1985
- // Prefer the local LLM/tool loop over the external adapter fallback whenever a client is available.
1986
- if (selectedRoute?.kind === "tool_loop") {
1987
- try {
1988
- this.emitCheckpoint("Tool loop started", "The model will choose tools from the active catalog.", eventContext, "execution");
1989
- const toolResult = await this.executeWithTools(prompt, eventContext, assistantBuffer, systemPrompt || undefined, executionGoalId);
1990
- const elapsed_ms = Date.now() - start;
1991
- if (this.hasUsage(toolResult.usage)) {
1992
- history.recordUsage("execution", toolResult.usage);
1993
- }
1994
- const diffArtifact = await collectGitDiffArtifact(gitRoot);
1995
- if (diffArtifact) {
1996
- this.emitDiffArtifact(diffArtifact, eventContext);
1997
- }
1998
- await history.appendAssistantMessage(toolResult.output);
1999
- this.emitCheckpoint("Response ready", "The tool-loop response has been persisted for this turn.", eventContext, "complete");
2000
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
2001
- this.emitEvent({
2002
- type: "assistant_final",
2003
- text: toolResult.output,
2004
- persisted: true,
2005
- ...this.eventBase(eventContext),
2006
- });
2007
- this.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
2008
- return { success: true, output: toolResult.output, elapsed_ms };
2009
- }
2010
- catch (err) {
2011
- const message = err instanceof Error ? err.message : String(err);
2012
- const output = this.emitLifecycleErrorEvent(message, assistantBuffer.text, eventContext);
2013
- this.emitLifecycleEndEvent("error", Date.now() - start, eventContext, false);
2014
- return {
2015
- success: false,
2016
- output,
2017
- elapsed_ms: Date.now() - start,
2018
- };
2019
- }
2020
- }
2021
- if (!resumeOnly && selectedRoute && selectedRoute.kind !== "adapter") {
2022
- const elapsed_ms = Date.now() - start;
2023
- const output = this.emitLifecycleErrorEvent(`Unsupported chat route: ${selectedRoute.kind}`, assistantBuffer.text, eventContext);
2024
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
2025
- return {
2026
- success: false,
2027
- output,
2028
- elapsed_ms,
2029
- };
2030
- }
2031
- const task = {
2032
- prompt,
2033
- timeout_ms: timeoutMs,
2034
- adapter_type: this.deps.adapter.adapterType,
2035
- cwd,
2036
- ...(systemPrompt ? { system_prompt: systemPrompt } : {}),
2037
- };
2038
- const resolvedTimeoutMs = task.timeout_ms ?? DEFAULT_TIMEOUT_MS;
2039
- this.emitCheckpoint("Adapter started", "The configured adapter has the current prompt and project context.", eventContext, "execution");
2040
- this.emitActivity("lifecycle", "Calling adapter...", eventContext, "lifecycle:adapter");
2041
- const adapterPromise = this.deps.adapter.execute(task);
2042
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Chat adapter timed out after ${resolvedTimeoutMs}ms`)), resolvedTimeoutMs));
2043
- let result;
2044
- try {
2045
- result = await Promise.race([adapterPromise, timeoutPromise]);
2046
- }
2047
- catch (err) {
2048
- const message = err instanceof Error ? err.message : String(err);
2049
- const output = this.emitLifecycleErrorEvent(message, assistantBuffer.text, eventContext);
2050
- const timeoutElapsedMs = Date.now() - start;
2051
- this.emitLifecycleEndEvent("error", timeoutElapsedMs, eventContext, false);
2052
- return {
2053
- success: false,
2054
- output,
2055
- elapsed_ms: timeoutElapsedMs,
2056
- };
2057
- }
2058
- // Surface adapter errors into output when output is empty
2059
- if (!result.output && result.error) {
2060
- result = { ...result, output: `Error: ${result.error}` };
2061
- }
2062
- const elapsed_ms = Date.now() - start;
2063
- if (result.output) {
2064
- this.pushAssistantDelta(result.output, assistantBuffer, eventContext);
2065
- }
2066
- // Verification loop: check if git has uncommitted changes; if so, run tests
2067
- const diffArtifact = await collectGitDiffArtifact(gitRoot);
2068
- if (diffArtifact) {
2069
- let retries = 0;
2070
- const VERIFY_TIMEOUT_MS = 30_000;
2071
- this.emitCheckpoint("Changes detected", "Verification is starting because the turn changed the working tree.", eventContext, "changes");
2072
- this.emitActivity("lifecycle", "Checking result...", eventContext, "lifecycle:checking");
2073
- let verification = await Promise.race([
2074
- verifyChatAction(gitRoot, this.deps.toolExecutor, { force: true }),
2075
- new Promise((resolve) => setTimeout(() => resolve({ passed: true }), VERIFY_TIMEOUT_MS)),
2076
- ]);
2077
- while (!verification.passed && retries < MAX_VERIFY_RETRIES) {
2078
- retries++;
2079
- this.emitCheckpoint("Verification retry", `Attempt ${retries} of ${MAX_VERIFY_RETRIES} is repairing failed checks.`, eventContext, `verification-retry-${retries}`);
2080
- const retryPrompt = `The previous changes caused test failures. Please fix them.\n\nTest output:\n${verification.testOutput ?? verification.errors.join("\n")}`;
2081
- const retryTask = { ...task, prompt: retryPrompt };
2082
- result = await this.deps.adapter.execute(retryTask);
2083
- verification = await verifyChatAction(gitRoot, this.deps.toolExecutor, { force: true });
2084
- }
2085
- if (!verification.passed) {
2086
- const finalDiffArtifact = await collectGitDiffArtifact(gitRoot);
2087
- if (finalDiffArtifact) {
2088
- this.emitDiffArtifact(finalDiffArtifact, eventContext);
2089
- }
2090
- this.emitCheckpoint("Verification failed", `Checks are still failing after ${MAX_VERIFY_RETRIES} retries.`, eventContext, "verification");
2091
- const failureOutput = this.emitLifecycleErrorEvent(`Changes applied but tests are still failing after ${MAX_VERIFY_RETRIES} retries.`, assistantBuffer.text, eventContext);
2092
- this.emitLifecycleEndEvent("error", Date.now() - start, eventContext, false);
2093
- return {
2094
- success: false,
2095
- output: `${failureOutput}\n\nTest output:\n${verification.testOutput ?? verification.errors.join("\n")}`.trim(),
2096
- elapsed_ms: Date.now() - start,
2097
- };
2098
- }
2099
- const finalDiffArtifact = await collectGitDiffArtifact(gitRoot);
2100
- if (finalDiffArtifact) {
2101
- this.emitDiffArtifact(finalDiffArtifact, eventContext);
2102
- }
2103
- this.emitCheckpoint("Verification passed", "Changed files passed the configured chat verification.", eventContext, "verification");
2104
- }
2105
- if (result.success) {
2106
- await history.appendAssistantMessage(result.output);
2107
- this.emitCheckpoint("Response ready", "The assistant response has been persisted for this turn.", eventContext, "complete");
2108
- this.emitActivity("lifecycle", "Finalizing response...", eventContext, "lifecycle:finalizing");
2109
- this.emitEvent({
2110
- type: "assistant_final",
2111
- text: result.output,
2112
- persisted: true,
2113
- ...this.eventBase(eventContext),
2114
- });
2115
- this.emitLifecycleEndEvent("completed", elapsed_ms, eventContext, true);
2116
- }
2117
- else {
2118
- const partialText = assistantBuffer.text !== result.output ? assistantBuffer.text : "";
2119
- result.output = this.emitLifecycleErrorEvent(result.output || result.error || "Unknown error", partialText, eventContext);
2120
- this.emitLifecycleEndEvent("error", elapsed_ms, eventContext, false);
2121
- }
2122
- return {
2123
- success: result.success,
2124
- output: result.output,
2125
- elapsed_ms,
2126
- };
2127
- }
2128
- async executeRuntimeControlRoute(route, runtimeControlContext, cwd, start) {
2129
- if (!this.deps.runtimeControlService) {
2130
- return {
2131
- success: false,
2132
- output: "Runtime control is not available in this chat surface yet.",
2133
- elapsed_ms: Date.now() - start,
2134
- };
2135
- }
2136
- const replyTarget = runtimeControlContext?.replyTarget ?? this.deps.runtimeReplyTarget;
2137
- const actor = runtimeControlContext?.actor ?? this.deps.runtimeControlActor;
2138
- const result = await this.deps.runtimeControlService.request({
2139
- intent: route.intent,
2140
- cwd,
2141
- requestedBy: actor ?? {
2142
- surface: replyTarget?.surface ?? "chat",
2143
- platform: replyTarget?.platform,
2144
- conversation_id: replyTarget?.conversation_id,
2145
- identity_key: replyTarget?.identity_key,
2146
- user_id: replyTarget?.user_id,
2147
- },
2148
- replyTarget: replyTarget ?? { surface: "chat" },
2149
- approvalFn: runtimeControlContext?.approvalFn
2150
- ?? this.deps.runtimeControlApprovalFn
2151
- ?? this.deps.approvalFn,
2152
- });
2153
- return {
2154
- success: result.success,
2155
- output: result.message,
2156
- elapsed_ms: Date.now() - start,
2157
- };
2158
- }
2159
- /**
2160
- * Execute a chat turn using llmClient with self-knowledge tools (function calling).
2161
- * Loops up to MAX_TOOL_LOOPS times to resolve tool calls, then returns final text.
2162
- */
2163
- async executeWithTools(prompt, eventContext, assistantBuffer, systemPrompt, goalId) {
2164
- const llmClient = this.deps.llmClient;
2165
- const messages = [{ role: "user", content: prompt }];
2166
- const toolCallContext = await this.buildToolCallContext(goalId);
2167
- const usage = this.zeroUsageCounter();
2168
- for (let loop = 0; loop < MAX_TOOL_LOOPS; loop++) {
2169
- // Recompute tools each iteration so newly activated deferred tools are included
2170
- const tools = this.deps.registry
2171
- ? toToolDefinitionsFiltered(this.deps.registry.listAll(), { activatedTools: this.activatedTools })
2172
- : [];
2173
- const supportsNativeToolCalling = llmClient.supportsToolCalling?.() !== false;
2174
- let response;
2175
- try {
2176
- this.emitActivity("lifecycle", "Calling model...", eventContext, "lifecycle:model");
2177
- response = await this.sendLLMMessage(llmClient, messages, {
2178
- ...(supportsNativeToolCalling
2179
- ? { tools, ...(systemPrompt ? { system: systemPrompt } : {}) }
2180
- : { system: buildPromptedToolProtocolSystemPrompt({ systemPrompt, tools }) }),
2181
- }, assistantBuffer, eventContext);
2182
- }
2183
- catch (err) {
2184
- console.error("[chat-runner] executeWithTools error:", err);
2185
- const hint = err instanceof Error ? `: ${err.message}` : "";
2186
- throw new Error(`Sorry, I encountered an error processing your request${hint}.`);
2187
- }
2188
- this.addUsageCounter(usage, this.usageFromLLMResponse(response));
2189
- const toolCalls = response.tool_calls?.length
2190
- ? response.tool_calls
2191
- : supportsNativeToolCalling
2192
- ? []
2193
- : extractPromptedToolCalls({
2194
- content: response.content,
2195
- tools,
2196
- createId: () => `prompted-${loop}-${crypto.randomUUID()}`,
2197
- }).map((call) => ({
2198
- id: call.id,
2199
- type: "function",
2200
- function: {
2201
- name: call.name,
2202
- arguments: JSON.stringify(call.input ?? {}),
2203
- },
2204
- }));
2205
- if (!supportsNativeToolCalling && toolCalls.length > 0) {
2206
- assistantBuffer.text = "";
2207
- }
2208
- // No tool calls — return the text content
2209
- if (toolCalls.length === 0) {
2210
- return {
2211
- output: assistantBuffer.text || response.content || "(no response)",
2212
- usage,
2213
- };
2214
- }
2215
- // Append assistant message, then process tool calls
2216
- messages.push({ role: "assistant", content: response.content || "" });
2217
- for (const tc of toolCalls) {
2218
- let args = {};
2219
- try {
2220
- args = JSON.parse(tc.function.arguments || "{}");
2221
- }
2222
- catch {
2223
- // ignore parse errors, use empty args
2224
- }
2225
- const toolResult = await this.dispatchToolCall(tc.id, tc.function.name, args, toolCallContext, eventContext);
2226
- // When ToolSearch returns results, activate deferred tools for subsequent turns
2227
- if (tc.function.name === "tool_search") {
2228
- this.activateToolSearchResults(toolResult);
2229
- }
2230
- messages.push({ role: "user", content: `Tool result for ${tc.function.name}:\n${toolResult}` });
2231
- }
2232
- }
2233
- // Max loops reached — return last assistant content or fallback
2234
- const lastAssistant = [...messages].reverse().find(m => m.role === "assistant");
2235
- return {
2236
- output: lastAssistant?.content || "I was unable to complete the request within the allowed tool call limit.",
2237
- usage,
2238
- };
2239
- }
2240
- /**
2241
- * Parse ToolSearch result JSON and activate any deferred tools found.
2242
- * Called after each tool_search execution so the LLM can call found tools on the next turn.
2243
- */
2244
- activateToolSearchResults(toolResult) {
2245
- try {
2246
- const parsed = JSON.parse(toolResult);
2247
- const results = Array.isArray(parsed) ? parsed : null;
2248
- if (results) {
2249
- for (const item of results) {
2250
- if (item && typeof item === "object" && typeof item["name"] === "string") {
2251
- this.activatedTools.add(item["name"]);
2252
- }
2253
- }
2254
- }
2255
- }
2256
- catch {
2257
- // Non-JSON result or unexpected shape — ignore
2258
- }
2259
- }
2260
- createAgentLoopEventSink(eventContext) {
2261
- return {
2262
- emit: async (event) => {
2263
- if (event.type === "tool_call_started") {
2264
- const detail = event.inputPreview ? previewActivityText(event.inputPreview) : undefined;
2265
- this.emitActivity("tool", formatToolActivity("Running", event.toolName, detail), eventContext, event.callId);
2266
- this.emitEvent({
2267
- type: "tool_start",
2268
- toolCallId: event.callId,
2269
- toolName: event.toolName,
2270
- args: this.parseAgentLoopPreview(event.inputPreview),
2271
- ...this.eventBase(eventContext),
2272
- });
2273
- this.emitEvent({
2274
- type: "tool_update",
2275
- toolCallId: event.callId,
2276
- toolName: event.toolName,
2277
- status: "running",
2278
- message: "started",
2279
- ...this.eventBase(eventContext),
2280
- });
2281
- return;
2282
- }
2283
- if (event.type === "tool_call_finished") {
2284
- this.emitActivity("tool", formatToolActivity(event.success ? "Finished" : "Failed", event.toolName, event.outputPreview), eventContext, event.callId);
2285
- this.emitEvent({
2286
- type: "tool_end",
2287
- toolCallId: event.callId,
2288
- toolName: event.toolName,
2289
- success: event.success,
2290
- summary: event.outputPreview,
2291
- durationMs: event.durationMs,
2292
- ...this.eventBase(eventContext),
2293
- });
2294
- return;
2295
- }
2296
- if (event.type === "assistant_message" && event.phase === "commentary" && event.contentPreview) {
2297
- this.emitActivity("commentary", previewActivityText(event.contentPreview, 120), eventContext, `commentary:${event.eventId}`);
2298
- return;
2299
- }
2300
- if (event.type === "plan_update") {
2301
- this.emitActivity("tool", `Updated plan: ${previewActivityText(event.summary)}`, eventContext, `plan:${event.turnId}`);
2302
- this.emitCheckpoint("Plan updated", previewActivityText(event.summary, 160), eventContext, `plan:${event.eventId}`);
2303
- this.emitEvent({
2304
- type: "tool_update",
2305
- toolCallId: `plan:${event.turnId}:${event.createdAt}`,
2306
- toolName: "update_plan",
2307
- status: "result",
2308
- message: event.summary,
2309
- ...this.eventBase(eventContext),
2310
- });
2311
- return;
2312
- }
2313
- if (event.type === "approval_request") {
2314
- this.emitActivity("tool", formatToolActivity("Running", event.toolName, `awaiting approval: ${event.reason}`), eventContext, event.callId);
2315
- this.emitCheckpoint("Approval requested", `${event.toolName}: ${event.reason}`, eventContext, `approval:${event.callId}`);
2316
- this.emitEvent({
2317
- type: "tool_update",
2318
- toolCallId: event.callId,
2319
- toolName: event.toolName,
2320
- status: "awaiting_approval",
2321
- message: event.reason,
2322
- ...this.eventBase(eventContext),
2323
- });
2324
- return;
2325
- }
2326
- if (event.type === "approval") {
2327
- this.emitActivity("tool", formatToolActivity("Finished", event.toolName, `approval ${event.status}: ${event.reason}`), eventContext);
2328
- this.emitEvent({
2329
- type: "tool_update",
2330
- toolCallId: `approval:${event.turnId}:${event.createdAt}`,
2331
- toolName: event.toolName,
2332
- status: "result",
2333
- message: `approval ${event.status}: ${event.reason}`,
2334
- ...this.eventBase(eventContext),
2335
- });
2336
- return;
2337
- }
2338
- if (event.type === "resumed") {
2339
- this.emitEvent({
2340
- type: "tool_update",
2341
- toolCallId: `resume:${event.turnId}:${event.createdAt}`,
2342
- toolName: "agentloop_resume",
2343
- status: "result",
2344
- message: `resumed ${event.restoredMessages} message(s) from ${event.fromUpdatedAt}`,
2345
- ...this.eventBase(eventContext),
2346
- });
2347
- return;
2348
- }
2349
- if (event.type === "context_compaction") {
2350
- this.emitEvent({
2351
- type: "tool_update",
2352
- toolCallId: `compaction:${event.turnId}:${event.createdAt}`,
2353
- toolName: "context_compaction",
2354
- status: "result",
2355
- message: `${event.phase} ${event.reason}: ${event.inputMessages} -> ${event.outputMessages}`,
2356
- ...this.eventBase(eventContext),
2357
- });
2358
- }
2359
- },
2360
- };
2361
- }
2362
- parseAgentLoopPreview(preview) {
2363
- try {
2364
- const parsed = JSON.parse(preview);
2365
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2366
- return parsed;
2367
- }
2368
- }
2369
- catch {
2370
- // fall through
2371
- }
2372
- return preview ? { preview } : {};
2373
- }
2374
- async resolveChatResumeSelector(selector) {
2375
- if (selector.startsWith("session:conversation:")) {
2376
- return { chatSelector: selector.slice("session:conversation:".length) };
2377
- }
2378
- if (selector.startsWith("session:") || selector.startsWith("run:")) {
2379
- const registry = createRuntimeSessionRegistry({ stateManager: this.deps.stateManager });
2380
- if (selector.startsWith("session:")) {
2381
- const session = await registry.getSession(selector);
2382
- if (session?.kind === "conversation") {
2383
- return { chatSelector: selector.slice("session:conversation:".length) };
2384
- }
2385
- if (session?.kind === "agent"
2386
- && session.resumable
2387
- && session.parent_session_id?.startsWith("session:conversation:")) {
2388
- return { chatSelector: session.parent_session_id.slice("session:conversation:".length) };
2389
- }
2390
- return {
2391
- chatSelector: selector,
2392
- nonResumableMessage: `Runtime session ${selector} is not chat-resumable. Inspect it with 'pulseed runtime session ${selector}'.`,
2393
- };
2394
- }
2395
- return {
2396
- chatSelector: selector,
2397
- nonResumableMessage: `Background run ${selector} is not chat-resumable. Inspect it with 'pulseed runtime run ${selector}'.`,
2398
- };
2399
- }
2400
- return { chatSelector: selector };
2401
- }
2402
- parseResumeCommand(input) {
2403
- const trimmed = input.trim();
2404
- const match = /^\/resume(?:\s+(.+))?$/i.exec(trimmed);
2405
- if (!match)
2406
- return null;
2407
- const selector = match[1]?.trim();
2408
- return selector ? { selector } : {};
2409
- }
2410
- async loadResumableAgentLoopState() {
2411
- if (!this.nativeAgentLoopStatePath)
2412
- return null;
2413
- const raw = await this.deps.stateManager.readRaw(this.nativeAgentLoopStatePath);
2414
- if (!this.isAgentLoopSessionState(raw))
2415
- return null;
2416
- if (raw.status === "completed")
2417
- return null;
2418
- return {
2419
- ...raw,
2420
- messages: [...raw.messages],
2421
- calledTools: [...raw.calledTools],
2422
- };
2423
- }
2424
- isAgentLoopSessionState(value) {
2425
- if (!value || typeof value !== "object")
2426
- return false;
2427
- const candidate = value;
2428
- return typeof candidate["sessionId"] === "string"
2429
- && typeof candidate["traceId"] === "string"
2430
- && typeof candidate["turnId"] === "string"
2431
- && typeof candidate["goalId"] === "string"
2432
- && typeof candidate["cwd"] === "string"
2433
- && typeof candidate["modelRef"] === "string"
2434
- && Array.isArray(candidate["messages"])
2435
- && Array.isArray(candidate["calledTools"])
2436
- && typeof candidate["status"] === "string";
2437
- }
2438
- /** Dispatch a tool call through the registry. */
2439
- async dispatchToolCall(toolCallId, name, args, context, eventContext) {
2440
- if (!this.deps.registry) {
2441
- this.emitActivity("tool", formatToolActivity("Failed", name, "No tool registry configured"), eventContext, toolCallId);
2442
- return JSON.stringify({ error: `No tool registry configured` });
2443
- }
2444
- const tool = this.deps.registry.get(name);
2445
- if (!tool) {
2446
- this.emitActivity("tool", formatToolActivity("Failed", name, `Unknown tool: ${name}`), eventContext, toolCallId);
2447
- return JSON.stringify({ error: `Unknown tool: ${name}` });
2448
- }
2449
- const startTime = Date.now();
2450
- try {
2451
- const parsed = tool.inputSchema.safeParse(args);
2452
- if (!parsed.success) {
2453
- this.emitActivity("tool", formatToolActivity("Failed", name, `Invalid input: ${parsed.error.message}`), eventContext, toolCallId);
2454
- this.emitEvent({
2455
- type: "tool_end",
2456
- toolCallId,
2457
- toolName: name,
2458
- success: false,
2459
- summary: `Invalid input: ${parsed.error.message}`,
2460
- durationMs: Date.now() - startTime,
2461
- ...this.eventBase(eventContext),
2462
- });
2463
- return JSON.stringify({ error: `Invalid input: ${parsed.error.message}` });
2464
- }
2465
- this.emitEvent({
2466
- type: "tool_start",
2467
- toolCallId,
2468
- toolName: name,
2469
- args,
2470
- ...this.eventBase(eventContext),
2471
- });
2472
- this.emitActivity("tool", formatToolActivity("Running", name, JSON.stringify(args)), eventContext, toolCallId);
2473
- let result;
2474
- if (this.deps.toolExecutor) {
2475
- this.emitEvent({
2476
- type: "tool_update",
2477
- toolCallId,
2478
- toolName: name,
2479
- status: "running",
2480
- message: "running",
2481
- ...this.eventBase(eventContext),
2482
- });
2483
- this.deps.onToolStart?.(name, args);
2484
- result = await this.deps.toolExecutor.execute(name, parsed.data, context);
2485
- }
2486
- else {
2487
- // Gate: check permissions before execution
2488
- const permResult = await tool.checkPermissions(parsed.data, context);
2489
- if (permResult.status === "denied") {
2490
- this.emitEvent({
2491
- type: "tool_end",
2492
- toolCallId,
2493
- toolName: name,
2494
- success: false,
2495
- summary: permResult.reason,
2496
- durationMs: Date.now() - startTime,
2497
- ...this.eventBase(eventContext),
2498
- });
2499
- return `Tool ${name} denied: ${permResult.reason}`;
2500
- }
2501
- if (permResult.status === "needs_approval") {
2502
- this.emitActivity("tool", formatToolActivity("Running", name, `awaiting approval: ${permResult.reason}`), eventContext, toolCallId);
2503
- this.emitEvent({
2504
- type: "tool_update",
2505
- toolCallId,
2506
- toolName: name,
2507
- status: "awaiting_approval",
2508
- message: permResult.reason,
2509
- ...this.eventBase(eventContext),
2510
- });
2511
- const approved = await context.approvalFn({
2512
- toolName: name,
2513
- input: parsed.data,
2514
- reason: permResult.reason,
2515
- permissionLevel: tool.metadata.permissionLevel,
2516
- isDestructive: tool.metadata.isDestructive,
2517
- reversibility: "unknown",
2518
- });
2519
- if (!approved) {
2520
- this.emitEvent({
2521
- type: "tool_end",
2522
- toolCallId,
2523
- toolName: name,
2524
- success: false,
2525
- summary: `Not approved: ${permResult.reason}`,
2526
- durationMs: Date.now() - startTime,
2527
- ...this.eventBase(eventContext),
2528
- });
2529
- return `Tool ${name} not approved: ${permResult.reason}`;
2530
- }
2531
- }
2532
- this.emitEvent({
2533
- type: "tool_update",
2534
- toolCallId,
2535
- toolName: name,
2536
- status: "running",
2537
- message: "running",
2538
- ...this.eventBase(eventContext),
2539
- });
2540
- this.deps.onToolStart?.(name, args);
2541
- result = await tool.call(parsed.data, context);
2542
- }
2543
- const durationMs = Date.now() - startTime;
2544
- this.deps.onToolEnd?.(name, { success: result.success, summary: result.summary || '...', durationMs });
2545
- this.emitActivity("tool", formatToolActivity(result.success ? "Finished" : "Failed", name, result.summary || "..."), eventContext, toolCallId);
2546
- this.emitEvent({
2547
- type: "tool_update",
2548
- toolCallId,
2549
- toolName: name,
2550
- status: "result",
2551
- message: result.summary || "...",
2552
- ...this.eventBase(eventContext),
2553
- });
2554
- this.emitEvent({
2555
- type: "tool_end",
2556
- toolCallId,
2557
- toolName: name,
2558
- success: result.success,
2559
- summary: result.summary || "...",
2560
- durationMs,
2561
- ...this.eventBase(eventContext),
2562
- });
2563
- // Prefer structured data (JSON) over plain summary so the LLM gets actionable content
2564
- return result.data != null ? JSON.stringify(result.data) : (result.summary ?? "(no result)");
2565
- }
2566
- catch (err) {
2567
- const message = err instanceof Error ? err.message : String(err);
2568
- const durationMs = Date.now() - startTime;
2569
- this.deps.onToolEnd?.(name, { success: false, summary: message, durationMs });
2570
- this.emitActivity("tool", formatToolActivity("Failed", name, message), eventContext, toolCallId);
2571
- this.emitEvent({
2572
- type: "tool_end",
2573
- toolCallId,
2574
- toolName: name,
2575
- success: false,
2576
- summary: message,
2577
- durationMs,
2578
- ...this.eventBase(eventContext),
2579
- });
2580
- return JSON.stringify({ error: `Tool ${name} failed: ${message}` });
2581
- }
2582
- }
2583
- async sendLLMMessage(llmClient, messages, options, assistantBuffer, eventContext) {
2584
- let streamed = false;
2585
- if (llmClient.sendMessageStream) {
2586
- const response = await llmClient.sendMessageStream(messages, options, {
2587
- onTextDelta: (delta) => {
2588
- streamed = true;
2589
- this.pushAssistantDelta(delta, assistantBuffer, eventContext);
2590
- },
2591
- });
2592
- if (!streamed && response.content) {
2593
- this.pushAssistantDelta(response.content, assistantBuffer, eventContext);
2594
- }
2595
- return response;
2596
- }
2597
- const response = await llmClient.sendMessage(messages, options);
2598
- if (response.content) {
2599
- this.pushAssistantDelta(response.content, assistantBuffer, eventContext);
2600
- }
2601
- return response;
2602
- }
2603
- createEventContext() {
2604
- return {
2605
- runId: crypto.randomUUID(),
2606
- turnId: crypto.randomUUID(),
2607
- };
2608
- }
2609
- eventBase(context) {
2610
- return { ...context, createdAt: new Date().toISOString() };
2611
- }
2612
- beginActiveTurn(context, cwd) {
2613
- let resolveFinished = () => { };
2614
- const finished = new Promise((resolve) => {
2615
- resolveFinished = resolve;
2616
- });
2617
- const turn = {
2618
- context,
2619
- cwd,
2620
- startedAt: Date.now(),
2621
- abortController: new AbortController(),
2622
- finished,
2623
- resolveFinished,
2624
- recentEvents: [],
2625
- interruptRequested: false,
2626
- };
2627
- this.activeTurn = turn;
2628
- return turn;
2629
- }
2630
- finishActiveTurn(context) {
2631
- const activeTurn = this.activeTurn;
2632
- if (!activeTurn || activeTurn.context.runId !== context.runId)
2633
- return;
2634
- activeTurn.resolveFinished();
2635
- this.activeTurn = null;
2636
- }
2637
- waitForActiveTurn(turn, timeoutMs) {
2638
- return Promise.race([
2639
- turn.finished.then(() => true),
2640
- new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
2641
- ]);
2642
- }
2643
- emitEphemeralAssistantResult(input, output, success, start) {
2644
- const context = this.createEventContext();
2645
- this.emitEvent({
2646
- type: "lifecycle_start",
2647
- input,
2648
- ...this.eventBase(context),
2649
- });
2650
- this.emitEvent({
2651
- type: "assistant_final",
2652
- text: output,
2653
- persisted: false,
2654
- ...this.eventBase(context),
2655
- });
2656
- const elapsed_ms = Date.now() - start;
2657
- this.emitLifecycleEndEvent(success ? "completed" : "error", elapsed_ms, context, false);
2658
- return { success, output, elapsed_ms };
2659
- }
2660
- rememberActiveTurnEvent(event) {
2661
- const activeTurn = this.activeTurn;
2662
- if (!activeTurn || activeTurn.context.turnId !== event.turnId)
2663
- return;
2664
- let summary = null;
2665
- if (event.type === "activity") {
2666
- summary = previewActivityText(event.message, 140);
2667
- }
2668
- else if (event.type === "tool_start") {
2669
- summary = `Started ${event.toolName}`;
2670
- }
2671
- else if (event.type === "tool_update") {
2672
- summary = `${event.toolName}: ${previewActivityText(event.message, 100)}`;
2673
- }
2674
- else if (event.type === "tool_end") {
2675
- summary = `${event.success ? "Finished" : "Failed"} ${event.toolName}: ${previewActivityText(event.summary, 100)}`;
2676
- }
2677
- if (!summary)
2678
- return;
2679
- activeTurn.recentEvents = [...activeTurn.recentEvents, summary].slice(-12);
2680
- }
2681
- emitEvent(event) {
2682
- this.rememberActiveTurnEvent(event);
2683
- const handler = this.onEvent ?? this.deps.onEvent;
2684
- handler?.(event);
2685
- }
2686
- emitActivity(kind, message, eventContext, sourceId, transient = true) {
2687
- if (!message.trim())
2688
- return;
2689
- this.emitEvent({
2690
- type: "activity",
2691
- kind,
2692
- message,
2693
- ...(sourceId ? { sourceId } : {}),
2694
- transient,
2695
- ...this.eventBase(eventContext),
2696
- });
2697
- }
2698
- emitIntent(input, selectedRoute, eventContext) {
2699
- const subject = formatIntentInput(input);
2700
- let nextStep = "resume the saved agent loop state before continuing.";
2701
- let reason = "resume needs the prior runtime context before any further action.";
2702
- if (selectedRoute?.kind === "runtime_control") {
2703
- nextStep = `prepare the ${selectedRoute.intent.kind} runtime-control request.`;
2704
- reason = "runtime changes need an explicit operation plan and approval path.";
2705
- }
2706
- else if (selectedRoute?.kind === "daemon_tend") {
2707
- nextStep = "convert the request into a daemon-backed CoreLoop goal confirmation.";
2708
- reason = "long-running work should survive chat turn timeouts and run through durable runtime state.";
2709
- }
2710
- else if (selectedRoute?.kind === "direct_answer") {
2711
- nextStep = "ask the lightweight model for a concise direct answer.";
2712
- reason = "the router classified this as a simple question that does not need tools.";
2713
- }
2714
- else if (selectedRoute?.kind === "agent_loop") {
2715
- nextStep = "gather workspace context, then let the agent loop inspect or change files with visible tool activity.";
2716
- reason = "this request may require multiple tool-backed steps.";
2717
- }
2718
- else if (selectedRoute?.kind === "tool_loop") {
2719
- nextStep = "call the model with the tool catalog, then execute selected tools with visible activity.";
2720
- reason = "the available tools are needed to answer from current project state.";
2721
- }
2722
- else if (selectedRoute?.kind === "adapter") {
2723
- nextStep = "prepare project context before handing the turn to the configured adapter.";
2724
- reason = "the adapter needs the current workspace context to act correctly.";
2725
- }
2726
- const message = [
2727
- "Intent",
2728
- `- Confirm: ${subject || "the current request"}`,
2729
- `- Next: ${nextStep}`,
2730
- `- Why: ${reason}`,
2731
- ].join("\n");
2732
- this.emitActivity("commentary", message, eventContext, "intent:first-step", false);
2733
- }
2734
- emitCheckpoint(title, detail, eventContext, sourceKey) {
2735
- const message = detail
2736
- ? `Checkpoint\n- ${title}: ${detail}`
2737
- : `Checkpoint\n- ${title}`;
2738
- this.emitActivity("checkpoint", message, eventContext, `checkpoint:${sourceKey}`, false);
2739
- }
2740
- emitDiffArtifact(artifact, eventContext) {
2741
- const sections = [
2742
- "Changed files",
2743
- "",
2744
- "Modified files",
2745
- artifact.nameStatus || artifact.stat,
2746
- "",
2747
- "Diff summary",
2748
- artifact.stat,
2749
- "",
2750
- "Inline patch",
2751
- "```diff",
2752
- artifact.patch || "(patch unavailable)",
2753
- artifact.truncated ? `... truncated after ${DIFF_ARTIFACT_MAX_LINES} lines; run /review for the full diff.` : "",
2754
- "```",
2755
- "",
2756
- "Files inspected are shown separately in the activity log.",
2757
- ].filter((line) => line !== "").join("\n");
2758
- this.emitActivity("diff", sections, eventContext, "diff:working-tree", false);
2759
- }
2760
- pushAssistantDelta(delta, assistantBuffer, eventContext) {
2761
- if (!delta)
2762
- return;
2763
- assistantBuffer.text += delta;
2764
- this.emitEvent({
2765
- type: "assistant_delta",
2766
- delta,
2767
- text: assistantBuffer.text,
2768
- ...this.eventBase(eventContext),
2769
- });
2770
- }
2771
- emitLifecycleEndEvent(status, elapsedMs, eventContext, persisted) {
2772
- this.emitEvent({
2773
- type: "lifecycle_end",
2774
- status,
2775
- elapsedMs,
2776
- persisted,
2777
- ...this.eventBase(eventContext),
2778
- });
2779
- this.finishActiveTurn(eventContext);
2780
- }
2781
- emitLifecycleErrorEvent(error, partialText, eventContext) {
2782
- const recovery = classifyFailureRecovery(error);
2783
- this.emitEvent({
2784
- type: "lifecycle_error",
2785
- error,
2786
- partialText,
2787
- persisted: false,
2788
- recovery,
2789
- ...this.eventBase(eventContext),
2790
- });
2791
- return formatLifecycleFailureMessage(error, partialText, recovery);
2792
- }
2793
- /** Build a ToolCallContext from ChatRunnerDeps for tool dispatch. */
2794
- async getSessionExecutionPolicy() {
2795
- if (this.sessionExecutionPolicy)
2796
- return this.sessionExecutionPolicy;
2797
- const config = await loadProviderConfig({ saveMigration: false });
2798
- this.sessionExecutionPolicy = resolveExecutionPolicy({
2799
- workspaceRoot: this.sessionCwd ?? process.cwd(),
2800
- security: config.agent_loop?.security,
2801
- });
2802
- return this.sessionExecutionPolicy;
2803
- }
2804
- async buildToolCallContext(goalId = this.deps.goalId) {
2805
- const executionPolicy = await this.getSessionExecutionPolicy();
2806
- return {
2807
- cwd: this.sessionCwd ?? process.cwd(),
2808
- goalId: goalId ?? "",
2809
- trustBalance: 0,
2810
- preApproved: false,
2811
- approvalFn: async (req) => {
2812
- if (this.deps.approvalFn) {
2813
- return this.deps.approvalFn(req.reason);
2814
- }
2815
- return false;
2816
- },
2817
- executionPolicy,
2818
- };
472
+ this.eventBridge.emitLifecycleEndEvent(result.success ? "completed" : "error", result.elapsed_ms, eventContext, false);
473
+ return result;
2819
474
  }
2820
475
  }
476
+ void COMMAND_HELP;
477
+ void formatRoute;
2821
478
  //# sourceMappingURL=chat-runner.js.map