@otto-code/server 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (734) hide show
  1. package/.env.example +31 -0
  2. package/README.md +111 -0
  3. package/dist/scripts/dev-runner.js +37 -0
  4. package/dist/scripts/mcp-stdio-socket-bridge-cli.mjs +62 -0
  5. package/dist/scripts/supervisor-entrypoint.js +141 -0
  6. package/dist/scripts/supervisor-log-config.js +31 -0
  7. package/dist/scripts/supervisor.js +252 -0
  8. package/dist/server/executable-resolution/executable-resolution.d.ts +10 -0
  9. package/dist/server/executable-resolution/executable-resolution.js +118 -0
  10. package/dist/server/executable-resolution/windows.d.ts +18 -0
  11. package/dist/server/executable-resolution/windows.js +65 -0
  12. package/dist/server/server/agent/activity-curator.d.ts +28 -0
  13. package/dist/server/server/agent/activity-curator.js +227 -0
  14. package/dist/server/server/agent/agent-archive.d.ts +11 -0
  15. package/dist/server/server/agent/agent-archive.js +16 -0
  16. package/dist/server/server/agent/agent-loading.d.ts +14 -0
  17. package/dist/server/server/agent/agent-loading.js +54 -0
  18. package/dist/server/server/agent/agent-manager.d.ts +470 -0
  19. package/dist/server/server/agent/agent-manager.js +3049 -0
  20. package/dist/server/server/agent/agent-personalities.d.ts +59 -0
  21. package/dist/server/server/agent/agent-personalities.js +103 -0
  22. package/dist/server/server/agent/agent-projections.d.ts +35 -0
  23. package/dist/server/server/agent/agent-projections.js +488 -0
  24. package/dist/server/server/agent/agent-prompt.d.ts +76 -0
  25. package/dist/server/server/agent/agent-prompt.js +230 -0
  26. package/dist/server/server/agent/agent-response-loop.d.ts +72 -0
  27. package/dist/server/server/agent/agent-response-loop.js +333 -0
  28. package/dist/server/server/agent/agent-sdk-types.d.ts +736 -0
  29. package/dist/server/server/agent/agent-sdk-types.js +22 -0
  30. package/dist/server/server/agent/agent-storage.d.ts +147 -0
  31. package/dist/server/server/agent/agent-storage.js +326 -0
  32. package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
  33. package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
  34. package/dist/server/server/agent/agent-timeline-store-types.d.ts +50 -0
  35. package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
  36. package/dist/server/server/agent/agent-timeline-store.d.ts +27 -0
  37. package/dist/server/server/agent/agent-timeline-store.js +230 -0
  38. package/dist/server/server/agent/audio-utils.d.ts +3 -0
  39. package/dist/server/server/agent/audio-utils.js +19 -0
  40. package/dist/server/server/agent/create-agent/create.d.ts +91 -0
  41. package/dist/server/server/agent/create-agent/create.js +370 -0
  42. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +54 -0
  43. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +152 -0
  44. package/dist/server/server/agent/create-agent-mode.d.ts +13 -0
  45. package/dist/server/server/agent/create-agent-mode.js +62 -0
  46. package/dist/server/server/agent/create-agent-title.d.ts +10 -0
  47. package/dist/server/server/agent/create-agent-title.js +34 -0
  48. package/dist/server/server/agent/dictation-debug.d.ts +13 -0
  49. package/dist/server/server/agent/dictation-debug.js +50 -0
  50. package/dist/server/server/agent/effort-levels.d.ts +2 -0
  51. package/dist/server/server/agent/effort-levels.js +7 -0
  52. package/dist/server/server/agent/foreground-run-state.d.ts +50 -0
  53. package/dist/server/server/agent/foreground-run-state.js +160 -0
  54. package/dist/server/server/agent/import-sessions.d.ts +47 -0
  55. package/dist/server/server/agent/import-sessions.js +145 -0
  56. package/dist/server/server/agent/lifecycle-command.d.ts +73 -0
  57. package/dist/server/server/agent/lifecycle-command.js +92 -0
  58. package/dist/server/server/agent/mcp-server.d.ts +5 -0
  59. package/dist/server/server/agent/mcp-server.js +66 -0
  60. package/dist/server/server/agent/mcp-shared.d.ts +192 -0
  61. package/dist/server/server/agent/mcp-shared.js +201 -0
  62. package/dist/server/server/agent/pcm16-resampler.d.ts +14 -0
  63. package/dist/server/server/agent/pcm16-resampler.js +63 -0
  64. package/dist/server/server/agent/permission-response.d.ts +15 -0
  65. package/dist/server/server/agent/permission-response.js +14 -0
  66. package/dist/server/server/agent/personality-stats-store.d.ts +23 -0
  67. package/dist/server/server/agent/personality-stats-store.js +65 -0
  68. package/dist/server/server/agent/prompt-attachments.d.ts +7 -0
  69. package/dist/server/server/agent/prompt-attachments.js +117 -0
  70. package/dist/server/server/agent/provider-history-timestamps.d.ts +2 -0
  71. package/dist/server/server/agent/provider-history-timestamps.js +16 -0
  72. package/dist/server/server/agent/provider-launch-config.d.ts +43 -0
  73. package/dist/server/server/agent/provider-launch-config.js +167 -0
  74. package/dist/server/server/agent/provider-notices.d.ts +3 -0
  75. package/dist/server/server/agent/provider-notices.js +5 -0
  76. package/dist/server/server/agent/provider-registry.d.ts +41 -0
  77. package/dist/server/server/agent/provider-registry.js +532 -0
  78. package/dist/server/server/agent/provider-session-import.d.ts +10 -0
  79. package/dist/server/server/agent/provider-session-import.js +49 -0
  80. package/dist/server/server/agent/provider-snapshot-manager.d.ts +142 -0
  81. package/dist/server/server/agent/provider-snapshot-manager.js +690 -0
  82. package/dist/server/server/agent/providers/acp-agent.d.ts +353 -0
  83. package/dist/server/server/agent/providers/acp-agent.js +2439 -0
  84. package/dist/server/server/agent/providers/claude/agent.d.ts +66 -0
  85. package/dist/server/server/agent/providers/claude/agent.js +4535 -0
  86. package/dist/server/server/agent/providers/claude/feature-definitions.d.ts +8 -0
  87. package/dist/server/server/agent/providers/claude/feature-definitions.js +24 -0
  88. package/dist/server/server/agent/providers/claude/model-manifest.d.ts +90 -0
  89. package/dist/server/server/agent/providers/claude/model-manifest.js +205 -0
  90. package/dist/server/server/agent/providers/claude/models.d.ts +11 -0
  91. package/dist/server/server/agent/providers/claude/models.js +97 -0
  92. package/dist/server/server/agent/providers/claude/partial-json.d.ts +5 -0
  93. package/dist/server/server/agent/providers/claude/partial-json.js +306 -0
  94. package/dist/server/server/agent/providers/claude/project-dir.d.ts +6 -0
  95. package/dist/server/server/agent/providers/claude/project-dir.js +57 -0
  96. package/dist/server/server/agent/providers/claude/query.d.ts +17 -0
  97. package/dist/server/server/agent/providers/claude/query.js +86 -0
  98. package/dist/server/server/agent/providers/claude/rewind.d.ts +30 -0
  99. package/dist/server/server/agent/providers/claude/rewind.js +30 -0
  100. package/dist/server/server/agent/providers/claude/sidechain-tracker.d.ts +22 -0
  101. package/dist/server/server/agent/providers/claude/sidechain-tracker.js +232 -0
  102. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +35 -0
  103. package/dist/server/server/agent/providers/claude/task-notification-tool-call.js +272 -0
  104. package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.d.ts +24 -0
  105. package/dist/server/server/agent/providers/claude/test-rewind-claude-sdk.js +23 -0
  106. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts +3 -0
  107. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +130 -0
  108. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +16 -0
  109. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +129 -0
  110. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +90 -0
  111. package/dist/server/server/agent/providers/codex/app-server-transport.js +262 -0
  112. package/dist/server/server/agent/providers/codex/rewind.d.ts +21 -0
  113. package/dist/server/server/agent/providers/codex/rewind.js +46 -0
  114. package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts +36 -0
  115. package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js +217 -0
  116. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +12 -0
  117. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +104 -0
  118. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +24 -0
  119. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +830 -0
  120. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +308 -0
  121. package/dist/server/server/agent/providers/codex-app-server-agent.js +4515 -0
  122. package/dist/server/server/agent/providers/codex-feature-definitions.d.ts +11 -0
  123. package/dist/server/server/agent/providers/codex-feature-definitions.js +45 -0
  124. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts +26 -0
  125. package/dist/server/server/agent/providers/copilot-acp-agent.js +188 -0
  126. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +15 -0
  127. package/dist/server/server/agent/providers/cursor-acp-agent.js +18 -0
  128. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +36 -0
  129. package/dist/server/server/agent/providers/diagnostic-utils.js +355 -0
  130. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +42 -0
  131. package/dist/server/server/agent/providers/generic-acp-agent.js +160 -0
  132. package/dist/server/server/agent/providers/kiro-acp-agent.d.ts +17 -0
  133. package/dist/server/server/agent/providers/kiro-acp-agent.js +78 -0
  134. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +85 -0
  135. package/dist/server/server/agent/providers/mock-load-test-agent.js +1010 -0
  136. package/dist/server/server/agent/providers/mock-slow-provider.d.ts +14 -0
  137. package/dist/server/server/agent/providers/mock-slow-provider.js +39 -0
  138. package/dist/server/server/agent/providers/openai-compat-agent.d.ts +422 -0
  139. package/dist/server/server/agent/providers/openai-compat-agent.js +2536 -0
  140. package/dist/server/server/agent/providers/openai-compat-feature-definitions.d.ts +40 -0
  141. package/dist/server/server/agent/providers/openai-compat-feature-definitions.js +82 -0
  142. package/dist/server/server/agent/providers/openai-compat-mcp.d.ts +87 -0
  143. package/dist/server/server/agent/providers/openai-compat-mcp.js +353 -0
  144. package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.d.ts +29 -0
  145. package/dist/server/server/agent/providers/openai-compat-otto-tool-permissions.js +96 -0
  146. package/dist/server/server/agent/providers/openai-compat-tools.d.ts +69 -0
  147. package/dist/server/server/agent/providers/openai-compat-tools.js +1224 -0
  148. package/dist/server/server/agent/providers/opencode/paths.d.ts +2 -0
  149. package/dist/server/server/agent/providers/opencode/paths.js +7 -0
  150. package/dist/server/server/agent/providers/opencode/rewind.d.ts +18 -0
  151. package/dist/server/server/agent/providers/opencode/rewind.js +14 -0
  152. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +91 -0
  153. package/dist/server/server/agent/providers/opencode/server-manager.js +425 -0
  154. package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +25 -0
  155. package/dist/server/server/agent/providers/opencode/test-server-manager.js +39 -0
  156. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.d.ts +92 -0
  157. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-harness.js +260 -0
  158. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts +3 -0
  159. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +256 -0
  160. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +13 -0
  161. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +53 -0
  162. package/dist/server/server/agent/providers/opencode-agent.d.ts +286 -0
  163. package/dist/server/server/agent/providers/opencode-agent.js +2830 -0
  164. package/dist/server/server/agent/providers/pi/agent.d.ts +132 -0
  165. package/dist/server/server/agent/providers/pi/agent.js +1649 -0
  166. package/dist/server/server/agent/providers/pi/cli-runtime.d.ts +21 -0
  167. package/dist/server/server/agent/providers/pi/cli-runtime.js +261 -0
  168. package/dist/server/server/agent/providers/pi/history-mapper.d.ts +9 -0
  169. package/dist/server/server/agent/providers/pi/history-mapper.js +140 -0
  170. package/dist/server/server/agent/providers/pi/rewind.d.ts +8 -0
  171. package/dist/server/server/agent/providers/pi/rewind.js +8 -0
  172. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +208 -0
  173. package/dist/server/server/agent/providers/pi/rpc-types.js +2 -0
  174. package/dist/server/server/agent/providers/pi/runtime.d.ts +57 -0
  175. package/dist/server/server/agent/providers/pi/runtime.js +56 -0
  176. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +16 -0
  177. package/dist/server/server/agent/providers/pi/session-descriptor.js +346 -0
  178. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +83 -0
  179. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +184 -0
  180. package/dist/server/server/agent/providers/pi/tool-call-mapper.d.ts +112 -0
  181. package/dist/server/server/agent/providers/pi/tool-call-mapper.js +284 -0
  182. package/dist/server/server/agent/providers/provider-image-output.d.ts +25 -0
  183. package/dist/server/server/agent/providers/provider-image-output.js +111 -0
  184. package/dist/server/server/agent/providers/provider-runner.d.ts +27 -0
  185. package/dist/server/server/agent/providers/provider-runner.js +81 -0
  186. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts +3 -0
  187. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js +60 -0
  188. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +884 -0
  189. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +778 -0
  190. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +25 -0
  191. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +208 -0
  192. package/dist/server/server/agent/providers/trae-acp-agent.d.ts +15 -0
  193. package/dist/server/server/agent/providers/trae-acp-agent.js +18 -0
  194. package/dist/server/server/agent/recordings-debug.d.ts +3 -0
  195. package/dist/server/server/agent/recordings-debug.js +19 -0
  196. package/dist/server/server/agent/rewind/rewind.d.ts +10 -0
  197. package/dist/server/server/agent/rewind/rewind.js +29 -0
  198. package/dist/server/server/agent/rewind/test-rewind-session.d.ts +48 -0
  199. package/dist/server/server/agent/rewind/test-rewind-session.js +88 -0
  200. package/dist/server/server/agent/runtime-mcp-config.d.ts +14 -0
  201. package/dist/server/server/agent/runtime-mcp-config.js +53 -0
  202. package/dist/server/server/agent/structured-generation-providers.d.ts +29 -0
  203. package/dist/server/server/agent/structured-generation-providers.js +236 -0
  204. package/dist/server/server/agent/stt-debug.d.ts +10 -0
  205. package/dist/server/server/agent/stt-debug.js +33 -0
  206. package/dist/server/server/agent/stt-manager.d.ts +38 -0
  207. package/dist/server/server/agent/stt-manager.js +246 -0
  208. package/dist/server/server/agent/system-prompt.d.ts +2 -0
  209. package/dist/server/server/agent/system-prompt.js +8 -0
  210. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  211. package/dist/server/server/agent/timeline-append.js +27 -0
  212. package/dist/server/server/agent/timeline-projection.d.ts +71 -0
  213. package/dist/server/server/agent/timeline-projection.js +373 -0
  214. package/dist/server/server/agent/tools/otto-tools.d.ts +80 -0
  215. package/dist/server/server/agent/tools/otto-tools.js +2912 -0
  216. package/dist/server/server/agent/tools/types.d.ts +36 -0
  217. package/dist/server/server/agent/tools/types.js +2 -0
  218. package/dist/server/server/agent/tts-debug.d.ts +8 -0
  219. package/dist/server/server/agent/tts-debug.js +24 -0
  220. package/dist/server/server/agent/tts-manager.d.ts +41 -0
  221. package/dist/server/server/agent/tts-manager.js +375 -0
  222. package/dist/server/server/agent-attention-policy.d.ts +29 -0
  223. package/dist/server/server/agent-attention-policy.js +36 -0
  224. package/dist/server/server/artifact/artifact-prompt.d.ts +2 -0
  225. package/dist/server/server/artifact/artifact-prompt.js +15 -0
  226. package/dist/server/server/artifact/artifact-service.d.ts +91 -0
  227. package/dist/server/server/artifact/artifact-service.js +419 -0
  228. package/dist/server/server/artifact/artifact-store.d.ts +24 -0
  229. package/dist/server/server/artifact/artifact-store.js +168 -0
  230. package/dist/server/server/artifact/artifact-watcher.d.ts +34 -0
  231. package/dist/server/server/artifact/artifact-watcher.js +146 -0
  232. package/dist/server/server/artifact/html-validator.d.ts +26 -0
  233. package/dist/server/server/artifact/html-validator.js +92 -0
  234. package/dist/server/server/atomic-file.d.ts +5 -0
  235. package/dist/server/server/atomic-file.js +19 -0
  236. package/dist/server/server/auth.d.ts +38 -0
  237. package/dist/server/server/auth.js +143 -0
  238. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +40 -0
  239. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +92 -0
  240. package/dist/server/server/auto-archive-on-merge/index.d.ts +8 -0
  241. package/dist/server/server/auto-archive-on-merge/index.js +15 -0
  242. package/dist/server/server/bootstrap.d.ts +127 -0
  243. package/dist/server/server/bootstrap.js +1331 -0
  244. package/dist/server/server/browser-tools/broker.d.ts +45 -0
  245. package/dist/server/server/browser-tools/broker.js +390 -0
  246. package/dist/server/server/browser-tools/errors.d.ts +22 -0
  247. package/dist/server/server/browser-tools/errors.js +27 -0
  248. package/dist/server/server/browser-tools/policy.d.ts +10 -0
  249. package/dist/server/server/browser-tools/policy.js +16 -0
  250. package/dist/server/server/browser-tools/tools.d.ts +24 -0
  251. package/dist/server/server/browser-tools/tools.js +967 -0
  252. package/dist/server/server/chat/chat-mentions.d.ts +47 -0
  253. package/dist/server/server/chat/chat-mentions.js +113 -0
  254. package/dist/server/server/chat/chat-service.d.ts +78 -0
  255. package/dist/server/server/chat/chat-service.js +337 -0
  256. package/dist/server/server/checkout/status-projection.d.ts +19 -0
  257. package/dist/server/server/checkout/status-projection.js +117 -0
  258. package/dist/server/server/checkout-diff-manager.d.ts +41 -0
  259. package/dist/server/server/checkout-diff-manager.js +189 -0
  260. package/dist/server/server/checkout-git-utils.d.ts +11 -0
  261. package/dist/server/server/checkout-git-utils.js +34 -0
  262. package/dist/server/server/client-message-id.d.ts +3 -0
  263. package/dist/server/server/client-message-id.js +12 -0
  264. package/dist/server/server/config.d.ts +22 -0
  265. package/dist/server/server/config.js +330 -0
  266. package/dist/server/server/connection-offer.d.ts +20 -0
  267. package/dist/server/server/connection-offer.js +59 -0
  268. package/dist/server/server/daemon-config-store.d.ts +44 -0
  269. package/dist/server/server/daemon-config-store.js +530 -0
  270. package/dist/server/server/daemon-keypair.d.ts +8 -0
  271. package/dist/server/server/daemon-keypair.js +42 -0
  272. package/dist/server/server/daemon-version.d.ts +5 -0
  273. package/dist/server/server/daemon-version.js +22 -0
  274. package/dist/server/server/daemon-worker.d.ts +2 -0
  275. package/dist/server/server/daemon-worker.js +251 -0
  276. package/dist/server/server/dictation/dictation-stream-manager.d.ts +87 -0
  277. package/dist/server/server/dictation/dictation-stream-manager.js +572 -0
  278. package/dist/server/server/exports.d.ts +24 -0
  279. package/dist/server/server/exports.js +24 -0
  280. package/dist/server/server/file-download/token-store.d.ts +24 -0
  281. package/dist/server/server/file-download/token-store.js +40 -0
  282. package/dist/server/server/file-explorer/code-index.d.ts +28 -0
  283. package/dist/server/server/file-explorer/code-index.js +127 -0
  284. package/dist/server/server/file-explorer/file-search.d.ts +72 -0
  285. package/dist/server/server/file-explorer/file-search.js +297 -0
  286. package/dist/server/server/file-explorer/file-watcher.d.ts +40 -0
  287. package/dist/server/server/file-explorer/file-watcher.js +0 -0
  288. package/dist/server/server/file-explorer/gitignore.d.ts +39 -0
  289. package/dist/server/server/file-explorer/gitignore.js +191 -0
  290. package/dist/server/server/file-explorer/service.d.ts +103 -0
  291. package/dist/server/server/file-explorer/service.js +420 -0
  292. package/dist/server/server/file-upload/index.d.ts +27 -0
  293. package/dist/server/server/file-upload/index.js +158 -0
  294. package/dist/server/server/hostnames.d.ts +13 -0
  295. package/dist/server/server/hostnames.js +94 -0
  296. package/dist/server/server/json-utils.d.ts +11 -0
  297. package/dist/server/server/json-utils.js +45 -0
  298. package/dist/server/server/lifecycle-reasons.d.ts +4 -0
  299. package/dist/server/server/lifecycle-reasons.js +6 -0
  300. package/dist/server/server/logger.d.ts +29 -0
  301. package/dist/server/server/logger.js +125 -0
  302. package/dist/server/server/loop-service.d.ts +234 -0
  303. package/dist/server/server/loop-service.js +804 -0
  304. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  305. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  306. package/dist/server/server/messages.d.ts +9 -0
  307. package/dist/server/server/messages.js +29 -0
  308. package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
  309. package/dist/server/server/migrations/backfill-workspace-id.migration.js +64 -0
  310. package/dist/server/server/otto-env.d.ts +13 -0
  311. package/dist/server/server/otto-env.js +53 -0
  312. package/dist/server/server/otto-home.d.ts +2 -0
  313. package/dist/server/server/otto-home.js +19 -0
  314. package/dist/server/server/otto-worktree-service.d.ts +52 -0
  315. package/dist/server/server/otto-worktree-service.js +294 -0
  316. package/dist/server/server/package-version.d.ts +19 -0
  317. package/dist/server/server/package-version.js +60 -0
  318. package/dist/server/server/pagination/cursor.d.ts +16 -0
  319. package/dist/server/server/pagination/cursor.js +62 -0
  320. package/dist/server/server/pagination/sortable-pager.d.ts +24 -0
  321. package/dist/server/server/pagination/sortable-pager.js +68 -0
  322. package/dist/server/server/pairing-offer.d.ts +18 -0
  323. package/dist/server/server/pairing-offer.js +47 -0
  324. package/dist/server/server/pairing-qr.d.ts +7 -0
  325. package/dist/server/server/pairing-qr.js +45 -0
  326. package/dist/server/server/path-utils.d.ts +5 -0
  327. package/dist/server/server/path-utils.js +34 -0
  328. package/dist/server/server/persisted-config.d.ts +417 -0
  329. package/dist/server/server/persisted-config.js +471 -0
  330. package/dist/server/server/persistence-hooks.d.ts +31 -0
  331. package/dist/server/server/persistence-hooks.js +93 -0
  332. package/dist/server/server/pid-lock.d.ts +32 -0
  333. package/dist/server/server/pid-lock.js +170 -0
  334. package/dist/server/server/preview/dev-server-manager.d.ts +108 -0
  335. package/dist/server/server/preview/dev-server-manager.js +426 -0
  336. package/dist/server/server/preview/launch-config.d.ts +37 -0
  337. package/dist/server/server/preview/launch-config.js +63 -0
  338. package/dist/server/server/preview/preview-tools.d.ts +30 -0
  339. package/dist/server/server/preview/preview-tools.js +249 -0
  340. package/dist/server/server/private-files.d.ts +6 -0
  341. package/dist/server/server/private-files.js +37 -0
  342. package/dist/server/server/process-diagnostics.d.ts +17 -0
  343. package/dist/server/server/process-diagnostics.js +22 -0
  344. package/dist/server/server/push/notifications.d.ts +9 -0
  345. package/dist/server/server/push/notifications.js +15 -0
  346. package/dist/server/server/push/push-service.d.ts +20 -0
  347. package/dist/server/server/push/push-service.js +66 -0
  348. package/dist/server/server/push/token-store.d.ts +18 -0
  349. package/dist/server/server/push/token-store.js +68 -0
  350. package/dist/server/server/relay-transport.d.ts +31 -0
  351. package/dist/server/server/relay-transport.js +514 -0
  352. package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
  353. package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
  354. package/dist/server/server/resolve-worktree-creation-intent.d.ts +32 -0
  355. package/dist/server/server/resolve-worktree-creation-intent.js +121 -0
  356. package/dist/server/server/schedule/cron.d.ts +4 -0
  357. package/dist/server/server/schedule/cron.js +81 -0
  358. package/dist/server/server/schedule/service.d.ts +89 -0
  359. package/dist/server/server/schedule/service.js +882 -0
  360. package/dist/server/server/schedule/store.d.ts +27 -0
  361. package/dist/server/server/schedule/store.js +190 -0
  362. package/dist/server/server/script-health-monitor.d.ts +39 -0
  363. package/dist/server/server/script-health-monitor.js +159 -0
  364. package/dist/server/server/script-proxy.d.ts +3 -0
  365. package/dist/server/server/script-proxy.js +2 -0
  366. package/dist/server/server/script-route-branch-handler.d.ts +10 -0
  367. package/dist/server/server/script-route-branch-handler.js +11 -0
  368. package/dist/server/server/script-status-projection.d.ts +36 -0
  369. package/dist/server/server/script-status-projection.js +200 -0
  370. package/dist/server/server/server-id.d.ts +17 -0
  371. package/dist/server/server/server-id.js +68 -0
  372. package/dist/server/server/service-proxy.d.ts +237 -0
  373. package/dist/server/server/service-proxy.js +714 -0
  374. package/dist/server/server/session/agent-config/agent-config-session.d.ts +59 -0
  375. package/dist/server/server/session/agent-config/agent-config-session.js +110 -0
  376. package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +65 -0
  377. package/dist/server/server/session/agent-updates/agent-updates-service.js +232 -0
  378. package/dist/server/server/session/artifact/artifact-session.d.ts +45 -0
  379. package/dist/server/server/session/artifact/artifact-session.js +293 -0
  380. package/dist/server/server/session/chat/chat-schedule-loop-session.d.ts +120 -0
  381. package/dist/server/server/session/chat/chat-schedule-loop-session.js +489 -0
  382. package/dist/server/server/session/checkout/checkout-session.d.ts +146 -0
  383. package/dist/server/server/session/checkout/checkout-session.js +1000 -0
  384. package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
  385. package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
  386. package/dist/server/server/session/daemon/daemon-self-update-session-controller.d.ts +32 -0
  387. package/dist/server/server/session/daemon/daemon-self-update-session-controller.js +88 -0
  388. package/dist/server/server/session/daemon/daemon-self-updater.d.ts +32 -0
  389. package/dist/server/server/session/daemon/daemon-self-updater.js +56 -0
  390. package/dist/server/server/session/daemon/daemon-session.d.ts +76 -0
  391. package/dist/server/server/session/daemon/daemon-session.js +148 -0
  392. package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
  393. package/dist/server/server/session/daemon/diagnostics.js +431 -0
  394. package/dist/server/server/session/daemon/install-origin.d.ts +7 -0
  395. package/dist/server/server/session/daemon/install-origin.js +64 -0
  396. package/dist/server/server/session/daemon/npm-global-cli.d.ts +29 -0
  397. package/dist/server/server/session/daemon/npm-global-cli.js +98 -0
  398. package/dist/server/server/session/files/workspace-files-session.d.ts +60 -0
  399. package/dist/server/server/session/files/workspace-files-session.js +518 -0
  400. package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
  401. package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
  402. package/dist/server/server/session/project-config/project-config-session.d.ts +34 -0
  403. package/dist/server/server/session/project-config/project-config-session.js +125 -0
  404. package/dist/server/server/session/provider/provider-catalog-session.d.ts +74 -0
  405. package/dist/server/server/session/provider/provider-catalog-session.js +343 -0
  406. package/dist/server/server/session/voice/voice-session.d.ts +167 -0
  407. package/dist/server/server/session/voice/voice-session.js +912 -0
  408. package/dist/server/server/session/voice/voice-turn-controller.d.ts +38 -0
  409. package/dist/server/server/session/voice/voice-turn-controller.js +393 -0
  410. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
  411. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
  412. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
  413. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
  414. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
  415. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
  416. package/dist/server/server/session.d.ts +450 -0
  417. package/dist/server/server/session.js +4763 -0
  418. package/dist/server/server/speech/audio.d.ts +10 -0
  419. package/dist/server/server/speech/audio.js +101 -0
  420. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  421. package/dist/server/server/speech/provider-resolver.js +7 -0
  422. package/dist/server/server/speech/providers/local/config.d.ts +30 -0
  423. package/dist/server/server/speech/providers/local/config.js +133 -0
  424. package/dist/server/server/speech/providers/local/models.d.ts +13 -0
  425. package/dist/server/server/speech/providers/local/models.js +21 -0
  426. package/dist/server/server/speech/providers/local/runtime.d.ts +31 -0
  427. package/dist/server/server/speech/providers/local/runtime.js +135 -0
  428. package/dist/server/server/speech/providers/local/sherpa/assets/silero_vad.onnx +0 -0
  429. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +109 -0
  430. package/dist/server/server/speech/providers/local/sherpa/model-catalog.js +129 -0
  431. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts +16 -0
  432. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +195 -0
  433. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.d.ts +49 -0
  434. package/dist/server/server/speech/providers/local/sherpa/sherpa-offline-recognizer.js +73 -0
  435. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts +9 -0
  436. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +107 -0
  437. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.d.ts +28 -0
  438. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-realtime-session.js +137 -0
  439. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.d.ts +21 -0
  440. package/dist/server/server/speech/providers/local/sherpa/sherpa-parakeet-stt.js +133 -0
  441. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts +18 -0
  442. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
  443. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.d.ts +24 -0
  444. package/dist/server/server/speech/providers/local/sherpa/sherpa-tts.js +145 -0
  445. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.d.ts +19 -0
  446. package/dist/server/server/speech/providers/local/sherpa/silero-vad-provider.js +49 -0
  447. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.d.ts +38 -0
  448. package/dist/server/server/speech/providers/local/sherpa/silero-vad-session.js +176 -0
  449. package/dist/server/server/speech/providers/local/sherpa/tts-voices.d.ts +10 -0
  450. package/dist/server/server/speech/providers/local/sherpa/tts-voices.js +86 -0
  451. package/dist/server/server/speech/providers/local/worker-bytes.d.ts +4 -0
  452. package/dist/server/server/speech/providers/local/worker-bytes.js +9 -0
  453. package/dist/server/server/speech/providers/local/worker-client.d.ts +93 -0
  454. package/dist/server/server/speech/providers/local/worker-client.js +591 -0
  455. package/dist/server/server/speech/providers/local/worker-process.d.ts +2 -0
  456. package/dist/server/server/speech/providers/local/worker-process.js +270 -0
  457. package/dist/server/server/speech/providers/local/worker-protocol.d.ts +96 -0
  458. package/dist/server/server/speech/providers/local/worker-protocol.js +2 -0
  459. package/dist/server/server/speech/providers/openai/config.d.ts +19 -0
  460. package/dist/server/server/speech/providers/openai/config.js +160 -0
  461. package/dist/server/server/speech/providers/openai/runtime.d.ts +29 -0
  462. package/dist/server/server/speech/providers/openai/runtime.js +102 -0
  463. package/dist/server/server/speech/providers/openai/stt.d.ts +23 -0
  464. package/dist/server/server/speech/providers/openai/stt.js +209 -0
  465. package/dist/server/server/speech/providers/openai/tts.d.ts +20 -0
  466. package/dist/server/server/speech/providers/openai/tts.js +70 -0
  467. package/dist/server/server/speech/speech-config-resolver.d.ts +11 -0
  468. package/dist/server/server/speech/speech-config-resolver.js +132 -0
  469. package/dist/server/server/speech/speech-provider.d.ts +71 -0
  470. package/dist/server/server/speech/speech-provider.js +2 -0
  471. package/dist/server/server/speech/speech-runtime.d.ts +67 -0
  472. package/dist/server/server/speech/speech-runtime.js +615 -0
  473. package/dist/server/server/speech/speech-settings-options.d.ts +14 -0
  474. package/dist/server/server/speech/speech-settings-options.js +73 -0
  475. package/dist/server/server/speech/speech-types.d.ts +22 -0
  476. package/dist/server/server/speech/speech-types.js +8 -0
  477. package/dist/server/server/speech/turn-detection-provider.d.ts +23 -0
  478. package/dist/server/server/speech/turn-detection-provider.js +2 -0
  479. package/dist/server/server/utils/diff-highlighter.d.ts +57 -0
  480. package/dist/server/server/utils/diff-highlighter.js +279 -0
  481. package/dist/server/server/voice-config.d.ts +15 -0
  482. package/dist/server/server/voice-config.js +54 -0
  483. package/dist/server/server/voice-permission-policy.d.ts +4 -0
  484. package/dist/server/server/voice-permission-policy.js +13 -0
  485. package/dist/server/server/voice-types.d.ts +12 -0
  486. package/dist/server/server/voice-types.js +2 -0
  487. package/dist/server/server/web-ui.d.ts +10 -0
  488. package/dist/server/server/web-ui.js +205 -0
  489. package/dist/server/server/websocket/runtime-metrics.d.ts +94 -0
  490. package/dist/server/server/websocket/runtime-metrics.js +148 -0
  491. package/dist/server/server/websocket-server.d.ts +187 -0
  492. package/dist/server/server/websocket-server.js +1766 -0
  493. package/dist/server/server/workspace-archive-service.d.ts +65 -0
  494. package/dist/server/server/workspace-archive-service.js +229 -0
  495. package/dist/server/server/workspace-auto-name.d.ts +55 -0
  496. package/dist/server/server/workspace-auto-name.js +128 -0
  497. package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
  498. package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
  499. package/dist/server/server/workspace-directory.d.ts +93 -0
  500. package/dist/server/server/workspace-directory.js +422 -0
  501. package/dist/server/server/workspace-git-metadata.d.ts +24 -0
  502. package/dist/server/server/workspace-git-metadata.js +55 -0
  503. package/dist/server/server/workspace-git-service.d.ts +270 -0
  504. package/dist/server/server/workspace-git-service.js +1423 -0
  505. package/dist/server/server/workspace-reconciliation-service.d.ts +57 -0
  506. package/dist/server/server/workspace-reconciliation-service.js +278 -0
  507. package/dist/server/server/workspace-registry-bootstrap.d.ts +13 -0
  508. package/dist/server/server/workspace-registry-bootstrap.js +121 -0
  509. package/dist/server/server/workspace-registry-model.d.ts +55 -0
  510. package/dist/server/server/workspace-registry-model.js +190 -0
  511. package/dist/server/server/workspace-registry.d.ts +117 -0
  512. package/dist/server/server/workspace-registry.js +194 -0
  513. package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
  514. package/dist/server/server/workspace-script-runtime-store.js +78 -0
  515. package/dist/server/server/workspace-service-env.d.ts +18 -0
  516. package/dist/server/server/workspace-service-env.js +85 -0
  517. package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
  518. package/dist/server/server/workspace-service-port-registry.js +59 -0
  519. package/dist/server/server/worktree/commands.d.ts +55 -0
  520. package/dist/server/server/worktree/commands.js +107 -0
  521. package/dist/server/server/worktree-bootstrap.d.ts +76 -0
  522. package/dist/server/server/worktree-bootstrap.js +720 -0
  523. package/dist/server/server/worktree-branch-name-generator.d.ts +35 -0
  524. package/dist/server/server/worktree-branch-name-generator.js +91 -0
  525. package/dist/server/server/worktree-core.d.ts +31 -0
  526. package/dist/server/server/worktree-core.js +105 -0
  527. package/dist/server/server/worktree-errors.d.ts +12 -0
  528. package/dist/server/server/worktree-errors.js +28 -0
  529. package/dist/server/server/worktree-session.d.ts +152 -0
  530. package/dist/server/server/worktree-session.js +466 -0
  531. package/dist/server/services/git-hosting/bitbucket-cloud-service.d.ts +24 -0
  532. package/dist/server/services/git-hosting/bitbucket-cloud-service.js +754 -0
  533. package/dist/server/services/git-hosting/github-hosting-service.d.ts +4 -0
  534. package/dist/server/services/git-hosting/github-hosting-service.js +11 -0
  535. package/dist/server/services/git-hosting/hosting-http-client.d.ts +20 -0
  536. package/dist/server/services/git-hosting/hosting-http-client.js +131 -0
  537. package/dist/server/services/git-hosting/request-cache.d.ts +20 -0
  538. package/dist/server/services/git-hosting/request-cache.js +68 -0
  539. package/dist/server/services/git-hosting/resolver.d.ts +32 -0
  540. package/dist/server/services/git-hosting/resolver.js +152 -0
  541. package/dist/server/services/git-hosting/router.d.ts +4 -0
  542. package/dist/server/services/git-hosting/router.js +105 -0
  543. package/dist/server/services/git-hosting/status-poller.d.ts +39 -0
  544. package/dist/server/services/git-hosting/status-poller.js +149 -0
  545. package/dist/server/services/git-hosting/types.d.ts +53 -0
  546. package/dist/server/services/git-hosting/types.js +75 -0
  547. package/dist/server/services/github-service.d.ts +339 -0
  548. package/dist/server/services/github-service.js +2109 -0
  549. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  550. package/dist/server/services/quota-fetcher/manifest.js +52 -0
  551. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  552. package/dist/server/services/quota-fetcher/provider.js +2 -0
  553. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  554. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  555. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  556. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  557. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  558. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  559. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  560. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  561. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  562. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  563. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  564. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  565. package/dist/server/services/quota-fetcher/providers/minimax.d.ts +29 -0
  566. package/dist/server/services/quota-fetcher/providers/minimax.js +227 -0
  567. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  568. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  569. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  570. package/dist/server/services/quota-fetcher/service.js +58 -0
  571. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  572. package/dist/server/services/quota-fetcher/usage.js +49 -0
  573. package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
  574. package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
  575. package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
  576. package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
  577. package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
  578. package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
  579. package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
  580. package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
  581. package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
  582. package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
  583. package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
  584. package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
  585. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
  586. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
  587. package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
  588. package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
  589. package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
  590. package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
  591. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
  592. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
  593. package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
  594. package/dist/server/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
  595. package/dist/server/terminal/terminal-capture.d.ts +12 -0
  596. package/dist/server/terminal/terminal-capture.js +43 -0
  597. package/dist/server/terminal/terminal-manager-factory.d.ts +6 -0
  598. package/dist/server/terminal/terminal-manager-factory.js +5 -0
  599. package/dist/server/terminal/terminal-manager.d.ts +78 -0
  600. package/dist/server/terminal/terminal-manager.js +319 -0
  601. package/dist/server/terminal/terminal-output-coalescer.d.ts +34 -0
  602. package/dist/server/terminal/terminal-output-coalescer.js +73 -0
  603. package/dist/server/terminal/terminal-restore.d.ts +18 -0
  604. package/dist/server/terminal/terminal-restore.js +52 -0
  605. package/dist/server/terminal/terminal-session-controller.d.ts +85 -0
  606. package/dist/server/terminal/terminal-session-controller.js +819 -0
  607. package/dist/server/terminal/terminal-ts-loader.mjs +20 -0
  608. package/dist/server/terminal/terminal-worker-process.d.ts +2 -0
  609. package/dist/server/terminal/terminal-worker-process.js +304 -0
  610. package/dist/server/terminal/terminal-worker-protocol.d.ts +119 -0
  611. package/dist/server/terminal/terminal-worker-protocol.js +2 -0
  612. package/dist/server/terminal/terminal.d.ts +151 -0
  613. package/dist/server/terminal/terminal.js +1155 -0
  614. package/dist/server/terminal/worker-terminal-manager.d.ts +20 -0
  615. package/dist/server/terminal/worker-terminal-manager.js +688 -0
  616. package/dist/server/utils/build-metadata-prompt.d.ts +19 -0
  617. package/dist/server/utils/build-metadata-prompt.js +29 -0
  618. package/dist/server/utils/checkout-git.d.ts +253 -0
  619. package/dist/server/utils/checkout-git.js +2241 -0
  620. package/dist/server/utils/directory-suggestions.d.ts +26 -0
  621. package/dist/server/utils/directory-suggestions.js +830 -0
  622. package/dist/server/utils/git-rev-parse-path.d.ts +3 -0
  623. package/dist/server/utils/git-rev-parse-path.js +21 -0
  624. package/dist/server/utils/github-remote.d.ts +9 -0
  625. package/dist/server/utils/github-remote.js +62 -0
  626. package/dist/server/utils/otto-config-file.d.ts +30 -0
  627. package/dist/server/utils/otto-config-file.js +90 -0
  628. package/dist/server/utils/path.d.ts +18 -0
  629. package/dist/server/utils/path.js +123 -0
  630. package/dist/server/utils/project-icon.d.ts +39 -0
  631. package/dist/server/utils/project-icon.js +364 -0
  632. package/dist/server/utils/promise-timeout.d.ts +9 -0
  633. package/dist/server/utils/promise-timeout.js +25 -0
  634. package/dist/server/utils/run-git-command.d.ts +37 -0
  635. package/dist/server/utils/run-git-command.js +232 -0
  636. package/dist/server/utils/spawn.d.ts +25 -0
  637. package/dist/server/utils/spawn.js +59 -0
  638. package/dist/server/utils/string-command-shell.d.ts +13 -0
  639. package/dist/server/utils/string-command-shell.js +37 -0
  640. package/dist/server/utils/tree-kill.d.ts +18 -0
  641. package/dist/server/utils/tree-kill.js +77 -0
  642. package/dist/server/utils/windows-command.d.ts +15 -0
  643. package/dist/server/utils/windows-command.js +41 -0
  644. package/dist/server/utils/worktree-metadata.d.ts +39 -0
  645. package/dist/server/utils/worktree-metadata.js +174 -0
  646. package/dist/server/utils/worktree.d.ts +205 -0
  647. package/dist/server/utils/worktree.js +1120 -0
  648. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css +1 -0
  649. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.br +0 -0
  650. package/dist/server/web-ui/_expo/static/css/xterm-3bb1704bf6cb0876640973dc0244b4cb.css.gz +0 -0
  651. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js +1 -0
  652. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.br +0 -0
  653. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-a25c6055b348629689f75618db2c83e1.js.gz +0 -0
  654. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js +1 -0
  655. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.br +0 -0
  656. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7dc26ea1611eb9d7c2971a6745e5bf5c.js.gz +0 -0
  657. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js +3199 -0
  658. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.br +0 -0
  659. package/dist/server/web-ui/_expo/static/js/web/index-ea91f496cee180fa5bf308c8cc177646.js.gz +0 -0
  660. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js +1 -0
  661. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.br +0 -0
  662. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-b6b853a967343babf305512fe8c5958c.js.gz +0 -0
  663. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js +3 -0
  664. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.br +0 -0
  665. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-17223e158406802adc686d02d946573d.js.gz +0 -0
  666. package/dist/server/web-ui/apple-touch-icon.png +0 -0
  667. package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/inter/400Regular/Inter_400Regular.51b6ad87261f18b6433ec52871ddfabc.ttf +0 -0
  668. package/dist/server/web-ui/assets/__node_modules/@expo-google-fonts/jetbrains-mono/400Regular/JetBrainsMono_400Regular.a0147b5ab9e4946e81879aef45313def.ttf +0 -0
  669. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  670. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  671. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  672. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  673. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  674. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  675. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  676. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  677. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  678. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  679. package/dist/server/web-ui/assets/__node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  680. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  681. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  682. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  683. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  684. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  685. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  686. package/dist/server/web-ui/assets/__node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  687. package/dist/server/web-ui/assets/assets/images/editor-apps/antigravity.6e91a685c33435e0b466a56db86cf141.png +0 -0
  688. package/dist/server/web-ui/assets/assets/images/editor-apps/clion.baa1f4eb572e962a6117bdbee95749e4.png +0 -0
  689. package/dist/server/web-ui/assets/assets/images/editor-apps/cursor.c31d6bce4fe9aadc3fe59962f4c4fcf3.png +0 -0
  690. package/dist/server/web-ui/assets/assets/images/editor-apps/finder.7f68fc2c475621a672e1be09309d5567.png +0 -0
  691. package/dist/server/web-ui/assets/assets/images/editor-apps/goland.0796ab2b9be2637fd1cdcde891fad3ca.png +0 -0
  692. package/dist/server/web-ui/assets/assets/images/editor-apps/intellij-idea.33bc9ff9c0815d8ad43f618c8c244401.png +0 -0
  693. package/dist/server/web-ui/assets/assets/images/editor-apps/phpstorm.c60b33bc7a8309143d232e14b76554c5.png +0 -0
  694. package/dist/server/web-ui/assets/assets/images/editor-apps/pycharm.88f25ac484989c45c25323e0adccfa9f.png +0 -0
  695. package/dist/server/web-ui/assets/assets/images/editor-apps/rider.1a1091dc3a0ede0461f0aa38a54f080a.png +0 -0
  696. package/dist/server/web-ui/assets/assets/images/editor-apps/rubymine.0e5b66fd7d43f8cbac0f05bba97c9a21.png +0 -0
  697. package/dist/server/web-ui/assets/assets/images/editor-apps/rustrover.3590ef05482369f41d3484c90272570d.png +0 -0
  698. package/dist/server/web-ui/assets/assets/images/editor-apps/vscode.832bdb4c685d930f1c864c793703600b.png +0 -0
  699. package/dist/server/web-ui/assets/assets/images/editor-apps/webstorm.aa5dc2cd8c20cc0a155c4c5c5ab3c5f5.png +0 -0
  700. package/dist/server/web-ui/assets/assets/images/editor-apps/zed.f3a670b7f9aa226da4fe53fb86f1abbd.png +0 -0
  701. package/dist/server/web-ui/assets/assets/images/favicon-dark-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
  702. package/dist/server/web-ui/assets/assets/images/favicon-dark-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
  703. package/dist/server/web-ui/assets/assets/images/favicon-dark.12984efe6d435a13794a74cd2f858dcd.png +0 -0
  704. package/dist/server/web-ui/assets/assets/images/favicon-light-attention.3b2c331d11122d8f743c73a5878593be.png +0 -0
  705. package/dist/server/web-ui/assets/assets/images/favicon-light-running.6077c2ab71021b31c82a42e0b42ad837.png +0 -0
  706. package/dist/server/web-ui/assets/assets/images/favicon-light.12984efe6d435a13794a74cd2f858dcd.png +0 -0
  707. package/dist/server/web-ui/assets/assets/images/notification-icon.2394c3b427c0f5ee2bb84864d8fb0f45.png +0 -0
  708. package/dist/server/web-ui/favicon.ico +0 -0
  709. package/dist/server/web-ui/index.html +147 -0
  710. package/dist/server/web-ui/index.html.br +0 -0
  711. package/dist/server/web-ui/index.html.gz +0 -0
  712. package/dist/server/web-ui/manifest.json +27 -0
  713. package/dist/server/web-ui/manifest.json.br +0 -0
  714. package/dist/server/web-ui/manifest.json.gz +0 -0
  715. package/dist/server/web-ui/metadata.json +1 -0
  716. package/dist/server/web-ui/metadata.json.br +1 -0
  717. package/dist/server/web-ui/metadata.json.gz +0 -0
  718. package/dist/server/web-ui/pwa-icon-192.png +0 -0
  719. package/dist/server/web-ui/pwa-icon-512.png +0 -0
  720. package/dist/server/web-ui/robots.txt +2 -0
  721. package/dist/src/executable-resolution/executable-resolution.js +118 -0
  722. package/dist/src/executable-resolution/windows.js +65 -0
  723. package/dist/src/server/agent/provider-launch-config.js +167 -0
  724. package/dist/src/server/otto-env.js +53 -0
  725. package/dist/src/server/otto-home.js +19 -0
  726. package/dist/src/server/persisted-config.js +471 -0
  727. package/dist/src/server/pid-lock.js +170 -0
  728. package/dist/src/server/private-files.js +37 -0
  729. package/dist/src/server/speech/providers/local/sherpa/sherpa-runtime-env.js +84 -0
  730. package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
  731. package/dist/src/terminal/shell-integration/zsh/otto-integration.zsh +32 -0
  732. package/dist/src/utils/spawn.js +59 -0
  733. package/dist/src/utils/windows-command.js +41 -0
  734. package/package.json +118 -0
@@ -0,0 +1,2536 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { buildCompatToolPreviewDetail, buildOpenAIToolsPayload, COMPAT_TOOL_SPECS, executeCompatTool, findCompatToolSpec, isPathInsideWorkspace, } from "./openai-compat-tools.js";
4
+ import { ottoToolGroupForName } from "@otto-code/protocol/provider-config";
5
+ import { buildOpenAICompatFeatures, normalizeOpenAICompatAutoCompact, normalizeOpenAICompatReasoningEffort, OPENAI_COMPAT_AUTO_COMPACT_FALLBACK, OPENAI_COMPAT_AUTO_COMPACT_VALUES, OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID, OPENAI_COMPAT_THINKING_OPTIONS, } from "./openai-compat-feature-definitions.js";
6
+ import { OpenAICompatMcpManager } from "./openai-compat-mcp.js";
7
+ import { ottoToolPermissionKind } from "./openai-compat-otto-tool-permissions.js";
8
+ import { stripInternalOttoMcpServer } from "../runtime-mcp-config.js";
9
+ /**
10
+ * Native provider for OpenAI-compatible HTTP endpoints (LM Studio, Ollama,
11
+ * vLLM, llama.cpp server, gateways). The daemon talks to the endpoint
12
+ * directly — model discovery via GET {base}/models, streaming chat via
13
+ * POST {base}/chat/completions. No external agent binary is involved, so
14
+ * availability means "the server is reachable", not "a CLI is installed".
15
+ *
16
+ * Tool support: the daemon is the tool runtime. Function-calling models get a
17
+ * built-in coding toolset (read/list/grep/write/edit/shell) executed in the
18
+ * agent's cwd, with permission gating per mode.
19
+ */
20
+ export const OPENAI_COMPAT_EXTENDS = "openai-compatible";
21
+ const DEFAULT_CATALOG_TIMEOUT_MS = 10000;
22
+ /** Upper bound on model→tool→model rounds within a single turn. */
23
+ const MAX_TOOL_ROUNDS = 50;
24
+ /**
25
+ * Per-tool-result budget for the compaction payload. Large results keep their
26
+ * head and tail — truncating harder starves the summarizer of the material
27
+ * (file contents, diffs, command output) the summary is supposed to preserve.
28
+ */
29
+ const TOOL_RESULT_HEAD_CHARS = 3000;
30
+ const TOOL_RESULT_TAIL_CHARS = 1000;
31
+ /**
32
+ * Compaction keeps the most recent slice of the conversation verbatim and only
33
+ * summarizes the older history before it. Summarization is lossy, so we confine
34
+ * it to the distant past where the loss is cheap; recent turns (the files just
35
+ * read, the diff just applied, the error being debugged) stay intact.
36
+ * Default only — tunable per provider via `compaction.keepRecentTokens`.
37
+ */
38
+ const COMPACTION_KEEP_RECENT_TOKENS = 20000;
39
+ /**
40
+ * Resolve the provider-level auto-compact default that applies when an agent
41
+ * has no explicit `auto_compact` feature value. `autoCompact: false` wins over
42
+ * any configured threshold.
43
+ */
44
+ function resolveAutoCompactDefault(compaction) {
45
+ if (compaction?.autoCompact === false) {
46
+ return "off";
47
+ }
48
+ if (typeof compaction?.thresholdPercent === "number") {
49
+ const candidate = String(compaction.thresholdPercent);
50
+ if (OPENAI_COMPAT_AUTO_COMPACT_VALUES.includes(candidate)) {
51
+ return candidate;
52
+ }
53
+ }
54
+ return OPENAI_COMPAT_AUTO_COMPACT_FALLBACK;
55
+ }
56
+ function resolveKeepRecentTokens(compaction) {
57
+ return typeof compaction?.keepRecentTokens === "number" && compaction.keepRecentTokens > 0
58
+ ? compaction.keepRecentTokens
59
+ : COMPACTION_KEEP_RECENT_TOKENS;
60
+ }
61
+ /**
62
+ * Pre-summarization pruning (zero-LLM) reclaims context before we spend a model
63
+ * call: uneventful tool results are elided and oversized older tool outputs are
64
+ * truncated, while the newest tool outputs are protected so live work is never
65
+ * trimmed out from under the model.
66
+ */
67
+ const PRUNE_PROTECT_RECENT_TOOL_TOKENS = 12000;
68
+ const PRUNE_TOOL_RESULT_MIN_CHARS = 2000;
69
+ const PRUNE_TOOL_RESULT_HEAD_CHARS = 800;
70
+ const PRUNE_TOOL_RESULT_TAIL_CHARS = 400;
71
+ const UNEVENTFUL_RESULT_PLACEHOLDER = "[Uneventful result elided]";
72
+ /**
73
+ * True when a tool result carries no signal worth keeping in context — empty
74
+ * output, zero-match searches, or a bare timeout/no-op acknowledgement. These
75
+ * are elided wholesale during pruning. Kept conservative on purpose: anything
76
+ * ambiguous is left intact for the summarizer to judge.
77
+ */
78
+ export function isUneventfulToolResult(content) {
79
+ const trimmed = content.trim();
80
+ if (trimmed.length === 0) {
81
+ return true;
82
+ }
83
+ if (trimmed === UNEVENTFUL_RESULT_PLACEHOLDER) {
84
+ return false;
85
+ }
86
+ return [
87
+ /^no matches found\.?$/iu,
88
+ /^no results? found\.?$/iu,
89
+ /^found 0 (?:matches|results|files)\b/iu,
90
+ /^0 results?\b/iu,
91
+ /^no files? found\.?$/iu,
92
+ /^\(?no output\)?\.?$/iu,
93
+ /^command (?:completed|finished|exited) with no output\.?$/iu,
94
+ /^no changes\b/iu,
95
+ ].some((pattern) => pattern.test(trimmed));
96
+ }
97
+ /** Built-in slash command available for every OpenAI-compatible session. */
98
+ const COMPACT_COMMAND = {
99
+ name: "compact",
100
+ description: "Compress the conversation history to free up context space",
101
+ argumentHint: "[instruction]",
102
+ kind: "command",
103
+ };
104
+ const CAPABILITIES = {
105
+ supportsStreaming: true,
106
+ supportsSessionPersistence: true,
107
+ supportsDynamicModes: false,
108
+ // The daemon itself is the MCP client for this provider: configured servers
109
+ // (provider config merged with per-agent config) are connected per session
110
+ // and their tools exposed to the model. See OpenAICompatMcpManager.
111
+ supportsMcpServers: true,
112
+ // The daemon owns this provider's tool loop, so we inject Otto's tool catalog
113
+ // (browser_*, preview_*, agent management, …) directly rather than via an MCP
114
+ // client the local model's runtime doesn't have. See ottoTools handling below.
115
+ supportsNativeOttoTools: true,
116
+ supportsReasoningStream: true,
117
+ supportsToolInvocations: true,
118
+ supportsRewindConversation: true,
119
+ supportsRewindFiles: false,
120
+ supportsRewindBoth: false,
121
+ };
122
+ // Icons/colorTiers live here (not in AGENT_PROVIDER_DEFINITIONS) because
123
+ // openai-compatible providers are registered dynamically, so the registry's
124
+ // definition-based mode decoration has nothing to merge from.
125
+ export const OPENAI_COMPAT_MODES = [
126
+ {
127
+ id: "default",
128
+ label: "Always Ask",
129
+ description: "Prompts for permission before running commands or editing files",
130
+ icon: "ShieldQuestionMark",
131
+ colorTier: "neutral",
132
+ },
133
+ {
134
+ id: "acceptEdits",
135
+ label: "Accept File Edits",
136
+ description: "Automatically approves file edits inside the workspace; still asks before running commands",
137
+ icon: "ShieldPerson",
138
+ colorTier: "safe",
139
+ },
140
+ {
141
+ id: "plan",
142
+ label: "Read Only",
143
+ description: "Only read tools are available — no edits or commands; web fetches still ask",
144
+ icon: "ShieldToggle",
145
+ colorTier: "planning",
146
+ },
147
+ {
148
+ id: "bypassPermissions",
149
+ label: "Bypass",
150
+ description: "Skip all permission prompts (use with caution)",
151
+ icon: "PrivacyTip",
152
+ colorTier: "dangerous",
153
+ isUnattended: true,
154
+ },
155
+ ];
156
+ const VALID_MODE_IDS = new Set(OPENAI_COMPAT_MODES.map((mode) => mode.id));
157
+ /** Permission-prompt description per builtin tool kind ("read" never prompts). */
158
+ const COMPAT_TOOL_PROMPT_DESCRIPTIONS = {
159
+ read: "Wants to read from the workspace",
160
+ edit: "Wants to modify a file",
161
+ execute: "Wants to run a shell command",
162
+ network: "Wants to fetch content from the web",
163
+ };
164
+ /** Mirror of the opencode provider's parser: `/name rest` → command + args. */
165
+ function parseSlashCommandInput(text) {
166
+ const trimmed = text.trim();
167
+ if (!trimmed.startsWith("/") || trimmed.length <= 1) {
168
+ return null;
169
+ }
170
+ const withoutPrefix = trimmed.slice(1);
171
+ const firstWhitespaceIdx = withoutPrefix.search(/\s/u);
172
+ const commandName = firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
173
+ if (!commandName || commandName.includes("/")) {
174
+ return null;
175
+ }
176
+ const rawArgs = firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
177
+ return { commandName, args: rawArgs.length > 0 ? rawArgs : null };
178
+ }
179
+ function toWireMessage(message) {
180
+ if (message.role === "system" || message.role === "user") {
181
+ return { role: message.role, content: message.content };
182
+ }
183
+ if (message.role === "assistant") {
184
+ return {
185
+ role: message.role,
186
+ content: message.content,
187
+ ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
188
+ };
189
+ }
190
+ return message;
191
+ }
192
+ /**
193
+ * Fold a reconstructed tool result's raw text into whichever field the
194
+ * preview detail's type uses for it. Only the flat result text survives
195
+ * persistence — structured metadata that a live run attaches (numMatches,
196
+ * webResults, exitCode, ...) isn't recoverable from a bare
197
+ * { name, arguments, result-text } tuple, so callers only get the text back.
198
+ */
199
+ function attachReconstructedOutput(detail, outputText) {
200
+ switch (detail.type) {
201
+ case "shell":
202
+ return { ...detail, output: outputText };
203
+ case "read":
204
+ return { ...detail, content: outputText };
205
+ case "search":
206
+ return { ...detail, content: outputText };
207
+ case "fetch":
208
+ return { ...detail, result: outputText };
209
+ case "plain_text":
210
+ return { ...detail, text: outputText };
211
+ case "unknown":
212
+ return { ...detail, output: outputText };
213
+ default:
214
+ return detail;
215
+ }
216
+ }
217
+ /**
218
+ * Reconstruct a tool_call timeline item from a persisted assistant tool call
219
+ * plus its matching "tool" result message. Used to replay history on resume,
220
+ * where the only surviving record is the raw chat-format conversation.
221
+ */
222
+ function buildReconstructedToolCallItem(call, resultMessage, cwd) {
223
+ let args = {};
224
+ try {
225
+ const parsed = call.function.arguments ? JSON.parse(call.function.arguments) : {};
226
+ if (parsed && typeof parsed === "object") {
227
+ args = parsed;
228
+ }
229
+ }
230
+ catch {
231
+ // Malformed persisted arguments shouldn't block reload — fall back to {}.
232
+ }
233
+ const baseDetail = buildCompatToolPreviewDetail(call.function.name, args, cwd);
234
+ return {
235
+ type: "tool_call",
236
+ callId: call.id,
237
+ name: call.function.name,
238
+ detail: resultMessage
239
+ ? attachReconstructedOutput(baseDetail, resultMessage.content)
240
+ : baseDetail,
241
+ // No matching "tool" result means the turn was interrupted before the
242
+ // result came back (e.g. the app closed mid-call).
243
+ status: resultMessage ? "completed" : "canceled",
244
+ error: null,
245
+ };
246
+ }
247
+ export function normalizeOpenAICompatBaseUrl(value) {
248
+ const withoutTrailingSlashes = value.trim().replace(/\/+$/u, "");
249
+ if (withoutTrailingSlashes.endsWith("/v1")) {
250
+ return withoutTrailingSlashes;
251
+ }
252
+ return `${withoutTrailingSlashes}/v1`;
253
+ }
254
+ function resolveEndpoint(env, label) {
255
+ const rawBaseUrl = env?.["OPENAI_BASE_URL"]?.trim();
256
+ if (!rawBaseUrl) {
257
+ throw new Error(`${label} has no server URL configured. Set OPENAI_BASE_URL in the provider settings.`);
258
+ }
259
+ const apiKey = env?.["OPENAI_API_KEY"]?.trim() || null;
260
+ return { baseUrl: normalizeOpenAICompatBaseUrl(rawBaseUrl), apiKey };
261
+ }
262
+ function buildHeaders(endpoint) {
263
+ const headers = { "Content-Type": "application/json" };
264
+ if (endpoint.apiKey) {
265
+ headers["Authorization"] = `Bearer ${endpoint.apiKey}`;
266
+ }
267
+ return headers;
268
+ }
269
+ function unreachableError(label, endpoint, cause) {
270
+ const detail = cause instanceof Error ? cause.message : String(cause);
271
+ return new Error(`Cannot reach ${label} at ${endpoint.baseUrl} (${detail}). Make sure the server is running and the URL is correct.`);
272
+ }
273
+ function promptToText(prompt) {
274
+ if (typeof prompt === "string") {
275
+ return prompt;
276
+ }
277
+ return prompt
278
+ .flatMap((block) => (block.type === "text" ? [block.text] : []))
279
+ .join("\n")
280
+ .trim();
281
+ }
282
+ async function* readLines(body) {
283
+ const decoder = new TextDecoder();
284
+ let buffer = "";
285
+ for await (const chunk of body) {
286
+ buffer += decoder.decode(chunk, { stream: true });
287
+ let newlineIndex = buffer.indexOf("\n");
288
+ while (newlineIndex !== -1) {
289
+ yield buffer.slice(0, newlineIndex).replace(/\r$/u, "");
290
+ buffer = buffer.slice(newlineIndex + 1);
291
+ newlineIndex = buffer.indexOf("\n");
292
+ }
293
+ }
294
+ const rest = buffer.trim();
295
+ if (rest) {
296
+ yield rest;
297
+ }
298
+ }
299
+ function parseToolCallDeltas(deltaRecord) {
300
+ const raw = deltaRecord.tool_calls;
301
+ if (!Array.isArray(raw)) {
302
+ return [];
303
+ }
304
+ return raw.flatMap((entry, position) => {
305
+ if (!entry || typeof entry !== "object") {
306
+ return [];
307
+ }
308
+ const record = entry;
309
+ const fn = record.function && typeof record.function === "object"
310
+ ? record.function
311
+ : {};
312
+ return [
313
+ {
314
+ index: typeof record.index === "number" ? record.index : position,
315
+ ...(typeof record.id === "string" && record.id ? { id: record.id } : {}),
316
+ ...(typeof fn.name === "string" && fn.name ? { name: fn.name } : {}),
317
+ ...(typeof fn.arguments === "string" ? { argumentsChunk: fn.arguments } : {}),
318
+ },
319
+ ];
320
+ });
321
+ }
322
+ function parseStreamChunk(json) {
323
+ const result = {
324
+ content: null,
325
+ reasoning: null,
326
+ toolCalls: [],
327
+ finishReason: null,
328
+ usage: null,
329
+ };
330
+ if (!json || typeof json !== "object") {
331
+ return result;
332
+ }
333
+ const chunk = json;
334
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
335
+ const firstChoice = choices[0];
336
+ if (firstChoice && typeof firstChoice === "object") {
337
+ const choiceRecord = firstChoice;
338
+ if (typeof choiceRecord.finish_reason === "string") {
339
+ result.finishReason = choiceRecord.finish_reason;
340
+ }
341
+ const delta = choiceRecord.delta;
342
+ if (delta && typeof delta === "object") {
343
+ const deltaRecord = delta;
344
+ if (typeof deltaRecord.content === "string" && deltaRecord.content.length > 0) {
345
+ result.content = deltaRecord.content;
346
+ }
347
+ const reasoning = deltaRecord.reasoning_content ?? deltaRecord.reasoning;
348
+ if (typeof reasoning === "string" && reasoning.length > 0) {
349
+ result.reasoning = reasoning;
350
+ }
351
+ result.toolCalls = parseToolCallDeltas(deltaRecord);
352
+ }
353
+ }
354
+ const usage = chunk.usage;
355
+ if (usage && typeof usage === "object") {
356
+ const usageRecord = usage;
357
+ result.usage = {
358
+ ...(typeof usageRecord.prompt_tokens === "number"
359
+ ? { inputTokens: usageRecord.prompt_tokens }
360
+ : {}),
361
+ ...(typeof usageRecord.completion_tokens === "number"
362
+ ? { outputTokens: usageRecord.completion_tokens }
363
+ : {}),
364
+ };
365
+ }
366
+ return result;
367
+ }
368
+ function parseModelList(json) {
369
+ if (!json || typeof json !== "object") {
370
+ return [];
371
+ }
372
+ const data = json.data;
373
+ if (!Array.isArray(data)) {
374
+ return [];
375
+ }
376
+ return data.flatMap((entry) => {
377
+ if (entry && typeof entry === "object" && typeof entry.id === "string") {
378
+ return [entry.id];
379
+ }
380
+ return [];
381
+ });
382
+ }
383
+ /**
384
+ * Optional per-model context-length fields seen across OpenAI-compatible
385
+ * servers. LM Studio's native listing reports `loaded_context_length` (the
386
+ * window the loaded instance actually runs with) ahead of the model's
387
+ * theoretical `max_context_length`; vLLM extends /v1/models with
388
+ * `max_model_len`; other gateways use `context_length`/`context_window`.
389
+ */
390
+ const MODEL_CONTEXT_LENGTH_FIELDS = [
391
+ "loaded_context_length",
392
+ "max_context_length",
393
+ "max_model_len",
394
+ "context_length",
395
+ "context_window",
396
+ ];
397
+ function parseModelContextLength(entry) {
398
+ for (const field of MODEL_CONTEXT_LENGTH_FIELDS) {
399
+ const value = entry[field];
400
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
401
+ return value;
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+ function parseModelContextLengths(json) {
407
+ const lengths = new Map();
408
+ if (!json || typeof json !== "object") {
409
+ return lengths;
410
+ }
411
+ const data = json.data;
412
+ if (!Array.isArray(data)) {
413
+ return lengths;
414
+ }
415
+ for (const entry of data) {
416
+ if (!entry || typeof entry !== "object")
417
+ continue;
418
+ const record = entry;
419
+ if (typeof record.id !== "string")
420
+ continue;
421
+ const contextLength = parseModelContextLength(record);
422
+ if (contextLength !== null) {
423
+ lengths.set(record.id, contextLength);
424
+ }
425
+ }
426
+ return lengths;
427
+ }
428
+ /**
429
+ * Rough token estimate for the breakdown split (~4 chars/token). The split is
430
+ * proportional only; the total is corrected to the server-measured
431
+ * prompt_tokens when one has been observed.
432
+ */
433
+ function estimateTokens(text) {
434
+ return Math.ceil(text.length / 4);
435
+ }
436
+ function parseToolCallPayloads(raw) {
437
+ if (!Array.isArray(raw)) {
438
+ return null;
439
+ }
440
+ const calls = raw.flatMap((entry) => {
441
+ if (!entry || typeof entry !== "object") {
442
+ return [];
443
+ }
444
+ const record = entry;
445
+ const fn = record.function && typeof record.function === "object"
446
+ ? record.function
447
+ : null;
448
+ if (typeof record.id !== "string" ||
449
+ !fn ||
450
+ typeof fn.name !== "string" ||
451
+ typeof fn.arguments !== "string") {
452
+ return [];
453
+ }
454
+ return [
455
+ { id: record.id, type: "function", function: { name: fn.name, arguments: fn.arguments } },
456
+ ];
457
+ });
458
+ return calls.length > 0 ? calls : null;
459
+ }
460
+ function restoreMessages(metadata) {
461
+ const raw = metadata?.["messages"];
462
+ if (!Array.isArray(raw)) {
463
+ return [];
464
+ }
465
+ const parsed = raw.flatMap((entry) => {
466
+ if (!entry || typeof entry !== "object") {
467
+ return [];
468
+ }
469
+ const record = entry;
470
+ const { role, content } = record;
471
+ if (typeof content !== "string") {
472
+ return [];
473
+ }
474
+ if (role === "system" || role === "user") {
475
+ const messageId = typeof record.messageId === "string" ? record.messageId : undefined;
476
+ const isCompactionSummary = record.isCompactionSummary === true;
477
+ return [
478
+ {
479
+ role,
480
+ content,
481
+ ...(messageId ? { messageId } : {}),
482
+ ...(isCompactionSummary ? { isCompactionSummary: true } : {}),
483
+ },
484
+ ];
485
+ }
486
+ if (role === "assistant") {
487
+ const toolCalls = parseToolCallPayloads(record.tool_calls);
488
+ const reasoning = typeof record.reasoning === "string" ? record.reasoning : undefined;
489
+ return [
490
+ {
491
+ role,
492
+ content,
493
+ ...(reasoning ? { reasoning } : {}),
494
+ ...(toolCalls ? { tool_calls: toolCalls } : {}),
495
+ },
496
+ ];
497
+ }
498
+ if (role === "tool" && typeof record.tool_call_id === "string") {
499
+ return [{ role, content, tool_call_id: record.tool_call_id }];
500
+ }
501
+ return [];
502
+ });
503
+ return sanitizeRestoredMessages(parsed);
504
+ }
505
+ /**
506
+ * Persistence trims to the last N messages, which can orphan tool messages
507
+ * from their assistant tool_calls (or vice versa). OpenAI-compatible servers
508
+ * reject such conversations, so repair the boundary: drop orphan tool
509
+ * results and strip tool_calls that have no results following them.
510
+ */
511
+ function sanitizeRestoredMessages(messages) {
512
+ const result = [];
513
+ for (let index = 0; index < messages.length; index += 1) {
514
+ const message = messages[index];
515
+ if (message.role === "tool") {
516
+ continue; // reached only when not consumed by an assistant tool_calls below
517
+ }
518
+ if (message.role !== "assistant" || !message.tool_calls) {
519
+ result.push(message);
520
+ continue;
521
+ }
522
+ const callIds = new Set(message.tool_calls.map((call) => call.id));
523
+ const results = [];
524
+ let cursor = index + 1;
525
+ while (cursor < messages.length) {
526
+ const candidate = messages[cursor];
527
+ if (candidate.role !== "tool" || !callIds.has(candidate.tool_call_id)) {
528
+ break;
529
+ }
530
+ results.push(candidate);
531
+ cursor += 1;
532
+ }
533
+ if (results.length === message.tool_calls.length) {
534
+ result.push(message, ...results);
535
+ index = cursor - 1;
536
+ }
537
+ else if (message.content || message.reasoning) {
538
+ result.push({
539
+ role: "assistant",
540
+ content: message.content,
541
+ ...(message.reasoning ? { reasoning: message.reasoning } : {}),
542
+ });
543
+ }
544
+ }
545
+ return result;
546
+ }
547
+ export class OpenAICompatAgentClient {
548
+ constructor(options) {
549
+ this.capabilities = CAPABILITIES;
550
+ this.provider = options.providerId;
551
+ this.logger = options.logger;
552
+ this.label = options.label;
553
+ this.env = options.env;
554
+ this.ottoToolGroups = options.ottoToolGroups ?? null;
555
+ this.mcpServers = options.mcpServers ?? null;
556
+ this.mcpToolPermissions = options.mcpToolPermissions ?? "always-ask";
557
+ this.compaction = options.compaction ?? null;
558
+ this.managedProcesses = options.managedProcesses ?? null;
559
+ }
560
+ async isAvailable() {
561
+ // Nothing to install — availability is endpoint reachability, surfaced
562
+ // through fetchCatalog so the UI shows a configuration error, not
563
+ // "not installed".
564
+ return true;
565
+ }
566
+ async fetchCatalog(options) {
567
+ const endpoint = resolveEndpoint(this.env, this.label);
568
+ const timeoutMs = options.timeoutMs ?? DEFAULT_CATALOG_TIMEOUT_MS;
569
+ let response;
570
+ try {
571
+ response = await fetch(`${endpoint.baseUrl}/models`, {
572
+ headers: buildHeaders(endpoint),
573
+ signal: AbortSignal.timeout(timeoutMs),
574
+ });
575
+ }
576
+ catch (error) {
577
+ throw unreachableError(this.label, endpoint, error);
578
+ }
579
+ if (!response.ok) {
580
+ throw new Error(`${this.label} at ${endpoint.baseUrl} responded ${response.status} to /models. Check the URL and API key.`);
581
+ }
582
+ const listing = await response.json();
583
+ const modelIds = parseModelList(listing);
584
+ const contextLengths = parseModelContextLengths(listing);
585
+ const models = modelIds.map((id, index) => {
586
+ const model = {
587
+ provider: this.provider,
588
+ id,
589
+ label: id,
590
+ isDefault: index === 0,
591
+ // Whether a given endpoint model honors reasoning_effort isn't
592
+ // discoverable from /models, so every model advertises the full set
593
+ // with "off" as the safe default (off omits the parameter entirely).
594
+ thinkingOptions: [...OPENAI_COMPAT_THINKING_OPTIONS],
595
+ defaultThinkingOptionId: OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID,
596
+ };
597
+ const contextWindowMaxTokens = contextLengths.get(id);
598
+ if (typeof contextWindowMaxTokens === "number") {
599
+ model.contextWindowMaxTokens = contextWindowMaxTokens;
600
+ }
601
+ return model;
602
+ });
603
+ return { models, modes: OPENAI_COMPAT_MODES };
604
+ }
605
+ async getDiagnostic() {
606
+ let endpoint;
607
+ try {
608
+ endpoint = resolveEndpoint(this.env, this.label);
609
+ }
610
+ catch (error) {
611
+ return { diagnostic: error instanceof Error ? error.message : String(error) };
612
+ }
613
+ try {
614
+ const catalog = await this.fetchCatalog({ scope: "global", force: true });
615
+ return {
616
+ diagnostic: [
617
+ `${this.label} (OpenAI-compatible endpoint)`,
618
+ ` URL: ${endpoint.baseUrl}`,
619
+ ` Auth: ${endpoint.apiKey ? "API key configured" : "none"}`,
620
+ ` Models: ${catalog.models.length}`,
621
+ ` Status: reachable`,
622
+ ].join("\n"),
623
+ };
624
+ }
625
+ catch (error) {
626
+ return {
627
+ diagnostic: [
628
+ `${this.label} (OpenAI-compatible endpoint)`,
629
+ ` URL: ${endpoint.baseUrl}`,
630
+ ` Auth: ${endpoint.apiKey ? "API key configured" : "none"}`,
631
+ ` Status: ${error instanceof Error ? error.message : String(error)}`,
632
+ ].join("\n"),
633
+ };
634
+ }
635
+ }
636
+ async listFeatures(config) {
637
+ const autoCompactDefault = resolveAutoCompactDefault(this.compaction);
638
+ const hideAutoCompact = this.compaction?.hideSelector === true;
639
+ return buildOpenAICompatFeatures({
640
+ autoCompact: hideAutoCompact
641
+ ? autoCompactDefault
642
+ : normalizeOpenAICompatAutoCompact(config.featureValues?.["auto_compact"], autoCompactDefault),
643
+ autoCompactDefault,
644
+ hideAutoCompact,
645
+ });
646
+ }
647
+ async createSession(config, launchContext) {
648
+ return new OpenAICompatAgentSession({
649
+ providerId: this.provider,
650
+ label: this.label,
651
+ env: this.env,
652
+ config,
653
+ sessionId: randomUUID(),
654
+ messages: [],
655
+ logger: this.logger,
656
+ ottoTools: launchContext?.ottoTools ?? null,
657
+ ottoToolGroups: this.ottoToolGroups,
658
+ mcpServers: this.mcpServers,
659
+ mcpToolPermissions: this.mcpToolPermissions,
660
+ compaction: this.compaction,
661
+ managedProcesses: this.managedProcesses,
662
+ });
663
+ }
664
+ async resumeSession(handle, overrides, launchContext) {
665
+ const metadata = (handle.metadata ?? {});
666
+ const model = typeof metadata.model === "string" ? metadata.model : undefined;
667
+ const modeId = typeof metadata.modeId === "string" ? metadata.modeId : undefined;
668
+ return new OpenAICompatAgentSession({
669
+ providerId: this.provider,
670
+ label: this.label,
671
+ env: this.env,
672
+ config: {
673
+ provider: this.provider,
674
+ cwd: overrides?.cwd ?? process.cwd(),
675
+ ...(model ? { model } : {}),
676
+ ...(modeId ? { modeId } : {}),
677
+ ...overrides,
678
+ },
679
+ sessionId: handle.sessionId,
680
+ messages: restoreMessages(metadata),
681
+ logger: this.logger,
682
+ ottoTools: launchContext?.ottoTools ?? null,
683
+ ottoToolGroups: this.ottoToolGroups,
684
+ mcpServers: this.mcpServers,
685
+ mcpToolPermissions: this.mcpToolPermissions,
686
+ compaction: this.compaction,
687
+ managedProcesses: this.managedProcesses,
688
+ });
689
+ }
690
+ }
691
+ function isZodType(schema) {
692
+ return typeof schema.safeParseAsync === "function";
693
+ }
694
+ /** Convert an Otto tool's Zod input schema into the JSON Schema the OpenAI `tools` payload expects. */
695
+ function ottoToolParameters(tool) {
696
+ const schema = tool.inputSchema;
697
+ if (!schema) {
698
+ return { type: "object", properties: {} };
699
+ }
700
+ try {
701
+ const zodSchema = isZodType(schema) ? schema : z.object(schema);
702
+ const jsonSchema = z.toJSONSchema(zodSchema);
703
+ delete jsonSchema.$schema;
704
+ return jsonSchema;
705
+ }
706
+ catch {
707
+ // A schema JSON Schema can't represent (e.g. transforms) still leaves the
708
+ // tool callable; executeTool validates the args when it runs.
709
+ return { type: "object", properties: {} };
710
+ }
711
+ }
712
+ /** Flatten an Otto tool result into the text fed back to the model. */
713
+ function ottoResultToText(result) {
714
+ const texts = result.content
715
+ .filter((part) => part.type === "text" && typeof part.text === "string")
716
+ .map((part) => part.text);
717
+ if (texts.length > 0) {
718
+ return texts.join("\n");
719
+ }
720
+ if (result.structuredContent !== undefined) {
721
+ try {
722
+ return JSON.stringify(result.structuredContent, null, 2);
723
+ }
724
+ catch {
725
+ return String(result.structuredContent);
726
+ }
727
+ }
728
+ return result.isError ? "Tool failed" : "Done.";
729
+ }
730
+ export class OpenAICompatAgentSession {
731
+ constructor(options) {
732
+ this.capabilities = CAPABILITIES;
733
+ this.listeners = new Set();
734
+ this.eventHistory = [];
735
+ this.pendingPermissions = new Map();
736
+ /** Connection failures already surfaced as timeline warnings. */
737
+ this.mcpFailuresAnnounced = false;
738
+ /**
739
+ * Loop protection: set when an auto-compaction failed or couldn't bring
740
+ * usage back under the threshold, so the next round doesn't immediately
741
+ * retry. Cleared once measured usage drops below the threshold again
742
+ * (rewind, manual /compact, model switch to a larger window).
743
+ */
744
+ this.autoCompactDisarmed = false;
745
+ this.activeTurn = null;
746
+ /** Resolved context window for the active model; null until (or unless) discovered. */
747
+ this.contextWindowMaxTokens = null;
748
+ /** Model the cached context window was resolved for; re-probe after a model switch. */
749
+ this.contextWindowProbedModel = null;
750
+ /** Exact context size (prompt + completion tokens) measured by the server on the last round. */
751
+ this.lastContextTokens = null;
752
+ this.provider = options.providerId;
753
+ this.label = options.label;
754
+ this.env = options.env;
755
+ this.logger = options.logger;
756
+ this.id = options.sessionId;
757
+ this.cwd = options.config.cwd;
758
+ this.config = options.config;
759
+ this.ottoTools = options.ottoTools ?? null;
760
+ this.ottoToolGroups = options.ottoToolGroups ?? null;
761
+ this.mcpToolPermissions = options.mcpToolPermissions ?? "always-ask";
762
+ // Provider-level servers merged with per-agent config; the per-agent entry
763
+ // wins per server name. The daemon-injected internal "otto" MCP server is
764
+ // stripped — this provider receives Otto tools natively, and connecting to
765
+ // it over MCP as well would double them.
766
+ const perAgentServers = stripInternalOttoMcpServer(options.config).mcpServers;
767
+ const mergedServers = {
768
+ ...options.mcpServers,
769
+ ...perAgentServers,
770
+ };
771
+ this.mcpManager =
772
+ Object.keys(mergedServers).length > 0
773
+ ? new OpenAICompatMcpManager({
774
+ servers: mergedServers,
775
+ providerId: options.providerId,
776
+ cwd: options.config.cwd,
777
+ logger: options.logger,
778
+ managedProcesses: options.managedProcesses ?? null,
779
+ })
780
+ : null;
781
+ this.modelId = options.config.model ?? null;
782
+ this.modeId =
783
+ options.config.modeId && VALID_MODE_IDS.has(options.config.modeId)
784
+ ? options.config.modeId
785
+ : "default";
786
+ // Effort comes from the model-level thinking option like every other
787
+ // provider. COMPAT(openaiCompatReasoningFeature): added in v0.4.5 — agents
788
+ // created before the unification persisted the value as the
789
+ // featureValues.reasoning_effort select instead; drop the fallback when
790
+ // floor >= v0.4.5 (target 2027-01).
791
+ this.reasoningEffort = normalizeOpenAICompatReasoningEffort(options.config.thinkingOptionId || options.config.featureValues?.["reasoning_effort"]);
792
+ this.autoCompactDefault = resolveAutoCompactDefault(options.compaction);
793
+ this.autoCompactHidden = options.compaction?.hideSelector === true;
794
+ this.autoCompact = this.autoCompactHidden
795
+ ? this.autoCompactDefault
796
+ : normalizeOpenAICompatAutoCompact(options.config.featureValues?.["auto_compact"], this.autoCompactDefault);
797
+ this.keepRecentTokens = resolveKeepRecentTokens(options.compaction);
798
+ // The system message is always rebuilt so cwd/mode/config changes take
799
+ // effect on resume; restored copies of it are dropped first.
800
+ this.messages = options.messages.filter((message) => message.role !== "system");
801
+ this.messages.unshift({ role: "system", content: this.buildSystemPrompt(options.config) });
802
+ this.rebuildEventHistory();
803
+ }
804
+ /**
805
+ * Rebuild replayable history from the current conversation so a resumed or
806
+ * rewound session still backfills its transcript, tool calls included.
807
+ * There is no separate durable store for tool traffic — `this.messages` is
808
+ * the only record that survives a resume, so tool_call items are
809
+ * reconstructed from each assistant message's `tool_calls` plus the
810
+ * matching "tool" result message. User messages keep their persisted
811
+ * messageId so the durable timeline and the provider conversation agree on
812
+ * rewind targets.
813
+ */
814
+ rebuildEventHistory() {
815
+ this.eventHistory.length = 0;
816
+ for (const [index, message] of this.messages.entries()) {
817
+ if (message.role === "user") {
818
+ if (!message.content)
819
+ continue;
820
+ this.eventHistory.push({
821
+ type: "timeline",
822
+ provider: this.provider,
823
+ item: {
824
+ type: "user_message",
825
+ text: message.content,
826
+ messageId: message.messageId ?? randomUUID(),
827
+ },
828
+ });
829
+ continue;
830
+ }
831
+ if (message.role !== "assistant")
832
+ continue;
833
+ // Live order within a round is reasoning → assistant text → tool calls;
834
+ // replay mirrors it.
835
+ if (message.reasoning) {
836
+ this.eventHistory.push({
837
+ type: "timeline",
838
+ provider: this.provider,
839
+ item: { type: "reasoning", text: message.reasoning },
840
+ });
841
+ }
842
+ if (message.content) {
843
+ this.eventHistory.push({
844
+ type: "timeline",
845
+ provider: this.provider,
846
+ item: { type: "assistant_message", text: message.content, messageId: randomUUID() },
847
+ });
848
+ }
849
+ for (const call of message.tool_calls ?? []) {
850
+ const resultMessage = this.messages
851
+ .slice(index + 1)
852
+ .find((candidate) => candidate.role === "tool" && candidate.tool_call_id === call.id);
853
+ this.eventHistory.push({
854
+ type: "timeline",
855
+ provider: this.provider,
856
+ item: buildReconstructedToolCallItem(call, resultMessage, this.cwd),
857
+ });
858
+ }
859
+ }
860
+ }
861
+ get features() {
862
+ return buildOpenAICompatFeatures({
863
+ autoCompact: this.autoCompact,
864
+ autoCompactDefault: this.autoCompactDefault,
865
+ hideAutoCompact: this.autoCompactHidden,
866
+ });
867
+ }
868
+ async setThinkingOption(thinkingOptionId) {
869
+ // null clears back to the model default; "off" omits reasoning_effort
870
+ // from requests entirely.
871
+ if (thinkingOptionId === null) {
872
+ this.reasoningEffort = OPENAI_COMPAT_DEFAULT_THINKING_OPTION_ID;
873
+ return;
874
+ }
875
+ if (normalizeOpenAICompatReasoningEffort(thinkingOptionId) !== thinkingOptionId) {
876
+ throw new Error(`Invalid effort option: ${String(thinkingOptionId)}`);
877
+ }
878
+ this.reasoningEffort = thinkingOptionId;
879
+ }
880
+ async applyPersonality(update) {
881
+ this.config.personalitySnapshot = update.personalitySnapshot;
882
+ this.config.systemPrompt = update.systemPrompt;
883
+ this.config.daemonAppendSystemPrompt = update.daemonAppendSystemPrompt;
884
+ // The daemon owns this conversation: the system prompt is just messages[0],
885
+ // re-sent wholesale on every request, so rebuilding it in place applies the
886
+ // new personality prompt from the very next turn — no session restart.
887
+ const rebuilt = { role: "system", content: this.buildSystemPrompt(this.config) };
888
+ if (this.messages[0]?.role === "system") {
889
+ this.messages[0] = rebuilt;
890
+ }
891
+ else {
892
+ this.messages.unshift(rebuilt);
893
+ }
894
+ }
895
+ async setFeature(featureId, value) {
896
+ // COMPAT(openaiCompatReasoningFeature): added in v0.4.5 — effort is no
897
+ // longer advertised as a feature, but old clients may still send the
898
+ // reasoning_effort select; drop when floor >= v0.4.5 (target 2027-01).
899
+ if (featureId === "reasoning_effort") {
900
+ if (normalizeOpenAICompatReasoningEffort(value) !== value) {
901
+ throw new Error(`Invalid reasoning effort value: ${String(value)}`);
902
+ }
903
+ this.reasoningEffort = value;
904
+ return;
905
+ }
906
+ if (featureId === "auto_compact") {
907
+ if (!OPENAI_COMPAT_AUTO_COMPACT_VALUES.includes(value)) {
908
+ throw new Error(`Invalid auto-compact value: ${String(value)}`);
909
+ }
910
+ this.autoCompact = value;
911
+ // A deliberate setting change is a fresh mandate — retry even if a
912
+ // previous auto-compaction was paused for lack of gain.
913
+ this.autoCompactDisarmed = false;
914
+ return;
915
+ }
916
+ throw new Error(`Unknown feature: ${featureId}`);
917
+ }
918
+ /**
919
+ * Live re-apply of provider-level compaction config so settings edits reach
920
+ * running chats without a restart. Sessions still sitting on the old default
921
+ * follow the new default; an explicit per-agent pick is kept unless the
922
+ * selector is hidden, which always forces the default.
923
+ */
924
+ applyCompactionConfig(compaction) {
925
+ const previousDefault = this.autoCompactDefault;
926
+ const previousHidden = this.autoCompactHidden;
927
+ const previousValue = this.autoCompact;
928
+ this.autoCompactDefault = resolveAutoCompactDefault(compaction);
929
+ this.autoCompactHidden = compaction?.hideSelector === true;
930
+ this.keepRecentTokens = resolveKeepRecentTokens(compaction);
931
+ if (this.autoCompactHidden || previousValue === previousDefault) {
932
+ this.autoCompact = this.autoCompactDefault;
933
+ }
934
+ if (this.autoCompact !== previousValue) {
935
+ // Same fresh-mandate rule as setFeature: a deliberate settings change
936
+ // re-arms a paused auto-compaction.
937
+ this.autoCompactDisarmed = false;
938
+ }
939
+ return (this.autoCompact !== previousValue ||
940
+ this.autoCompactHidden !== previousHidden ||
941
+ this.autoCompactDefault !== previousDefault);
942
+ }
943
+ buildSystemPrompt(config) {
944
+ const parts = [
945
+ [
946
+ `You are a coding agent running inside Otto against ${this.label}.`,
947
+ `Workspace directory: ${this.cwd}`,
948
+ `Platform: ${process.platform}`,
949
+ "Use the available tools to read, search, and modify files and to run shell commands.",
950
+ "Prefer tools over guessing: inspect files before editing them and verify your changes.",
951
+ "When the task is complete, reply with a concise summary of what you did.",
952
+ ].join("\n"),
953
+ this.buildPreviewWorkflowPrompt(),
954
+ config.systemPrompt?.trim(),
955
+ config.daemonAppendSystemPrompt?.trim(),
956
+ ];
957
+ return parts.filter((part) => Boolean(part)).join("\n\n");
958
+ }
959
+ /**
960
+ * Workflow doctrine for the preview/browser tool families. Tool descriptions
961
+ * alone don't reliably steer local models, so when both families are exposed
962
+ * the system prompt spells out the split: preview_start owns dev servers and
963
+ * their designated tab; browser_new_tab is for everything else.
964
+ */
965
+ buildPreviewWorkflowPrompt() {
966
+ const hasTool = (name) => Boolean(this.ottoTools?.getTool(name)) && this.isOttoToolGroupEnabled(name);
967
+ const hasPreview = hasTool("preview_start");
968
+ const hasBrowser = hasTool("browser_snapshot");
969
+ if (!hasPreview && !hasBrowser) {
970
+ return null;
971
+ }
972
+ const lines = ["## Verifying changes in the browser"];
973
+ if (hasPreview) {
974
+ lines.push("- Start dev servers with preview_start — never with run_command or other shell commands. It manages the process, its logs (preview_logs), and its preview tab.");
975
+ }
976
+ if (hasPreview && hasBrowser) {
977
+ lines.push("- preview_start returns browser.browserId: the server's designated preview tab, the same tab the user watches. Verify your changes against that browserId with browser_snapshot, browser_inspect, browser_logs, browser_click, and browser_screenshot.", "- Never open a dev server URL with browser_new_tab or in another tab — the daemon rejects it. browser_new_tab is only for external sites and general browsing.");
978
+ }
979
+ if (hasBrowser) {
980
+ lines.push("- After changing code that a running dev server renders, verify it: reload or snapshot the page, check browser_logs for errors, then share proof (a snapshot or screenshot) instead of asking the user to check manually.");
981
+ }
982
+ return lines.join("\n");
983
+ }
984
+ availableToolSpecs() {
985
+ return COMPAT_TOOL_SPECS.filter((spec) => {
986
+ // Read-only "plan" mode offers no actions against the local machine.
987
+ // "network" tools (web_fetch) stay available for research but prompt.
988
+ if (this.modeId === "plan" && spec.kind !== "read" && spec.kind !== "network")
989
+ return false;
990
+ // The builtin web tools (web_search/web_fetch) are gated by the "web"
991
+ // tool group, just like the injected Otto tool groups.
992
+ if (ottoToolGroupForName(spec.name) === "web" && !this.isOttoToolGroupEnabled(spec.name)) {
993
+ return false;
994
+ }
995
+ return true;
996
+ });
997
+ }
998
+ toolNeedsApproval(spec, args) {
999
+ if (spec.kind === "read")
1000
+ return false;
1001
+ if (this.modeId === "bypassPermissions")
1002
+ return false;
1003
+ if (this.modeId === "acceptEdits" && spec.kind === "edit") {
1004
+ // Auto-approval is scoped to the workspace: a write to ~/.bashrc or
1005
+ // Otto's own config is not the "file edits" the mode label promises,
1006
+ // so anything outside the cwd subtree still prompts.
1007
+ const target = args["path"];
1008
+ return typeof target !== "string" || !isPathInsideWorkspace(this.cwd, target);
1009
+ }
1010
+ // Everything else that can act — execute always, and network because an
1011
+ // unprompted web_fetch is an exfiltration channel for unprompted reads.
1012
+ return true;
1013
+ }
1014
+ /**
1015
+ * Otto's injected catalog (browser_*, preview_*, agent management, …) rendered
1016
+ * as OpenAI function specs so the local model can call them. Excluded in
1017
+ * read-only "plan" mode since they can take actions; builtin coding tools win
1018
+ * on name collisions. Empty when no catalog was injected (e.g. tools disabled).
1019
+ */
1020
+ /** Whether an Otto tool's group is enabled for this provider (null groups = all). */
1021
+ isOttoToolGroupEnabled(name) {
1022
+ const groups = this.ottoToolGroups;
1023
+ return !groups || groups.includes(ottoToolGroupForName(name));
1024
+ }
1025
+ /**
1026
+ * Connect the configured MCP servers before the first model round. A server
1027
+ * that fails to connect is skipped — surfaced once as a timeline warning,
1028
+ * never fatal to the session.
1029
+ */
1030
+ async ensureMcpReady(turn) {
1031
+ if (!this.mcpManager) {
1032
+ return;
1033
+ }
1034
+ await this.mcpManager.ensureConnected();
1035
+ if (this.mcpFailuresAnnounced) {
1036
+ return;
1037
+ }
1038
+ this.mcpFailuresAnnounced = true;
1039
+ for (const failure of this.mcpManager.failures) {
1040
+ this.emit({
1041
+ type: "timeline",
1042
+ provider: this.provider,
1043
+ turnId: turn.turnId,
1044
+ item: {
1045
+ type: "error",
1046
+ message: `MCP server '${failure.name}' unavailable: ${failure.error}`,
1047
+ },
1048
+ });
1049
+ }
1050
+ }
1051
+ /** Built-in slash commands plus MCP prompts from connected servers. */
1052
+ async listCommands() {
1053
+ const commands = [COMPACT_COMMAND];
1054
+ if (!this.mcpManager) {
1055
+ return commands;
1056
+ }
1057
+ const prompts = await this.mcpManager.listPrompts();
1058
+ return commands.concat(prompts.map((prompt) => ({
1059
+ name: prompt.commandName,
1060
+ description: prompt.description ?? `MCP prompt from server '${prompt.serverName}'`,
1061
+ argumentHint: prompt.argumentNames.join(" "),
1062
+ })));
1063
+ }
1064
+ /**
1065
+ * Handle `/compact [instruction]` by summarizing the older conversation
1066
+ * history while keeping the most recent slice verbatim, then splicing the
1067
+ * summary in ahead of the retained tail. Runs entirely in-process — no
1068
+ * external compaction service. Returns the turn's usage so the caller can
1069
+ * attach it to turn_completed.
1070
+ *
1071
+ * Pipeline (adapted from oh-my-pi's compaction design):
1072
+ * 1. Prune tool outputs (zero-LLM): elide uneventful results, truncate
1073
+ * oversized older ones, protect the newest.
1074
+ * 2. Split at the keep-recent boundary: [system] + [older → summarize] +
1075
+ * [recent → keep verbatim].
1076
+ * 3. Summarize the older region. If a prior compaction summary is present,
1077
+ * merge into it (incremental update) instead of re-summarizing a summary.
1078
+ * 4. Rebuild: [system, summary, ...recent].
1079
+ */
1080
+ async handleCompact(turn, instruction, trigger) {
1081
+ this.emit({
1082
+ type: "timeline",
1083
+ provider: this.provider,
1084
+ turnId: turn.turnId,
1085
+ item: { type: "compaction", status: "loading", trigger },
1086
+ });
1087
+ // Pre-compaction context size: prefer the server-measured figure from the
1088
+ // last round (what the context ring currently shows); fall back to a local
1089
+ // estimate of the full request payload.
1090
+ const preTokens = this.lastContextTokens ?? this.estimateFullContextTokens();
1091
+ // 1. Zero-LLM pruning first, so both the summary input and the retained
1092
+ // tail are already free of noise and oversized old tool dumps.
1093
+ this.pruneToolOutputs();
1094
+ // 2. Split at the keep-recent boundary. keepFromIndex is the first message
1095
+ // (after the system prompt at index 0) that stays verbatim.
1096
+ const keepFromIndex = this.computeCompactionKeepFromIndex();
1097
+ const summarizeRegion = this.messages.slice(1, keepFromIndex);
1098
+ const keptRegion = this.messages.slice(keepFromIndex);
1099
+ if (summarizeRegion.length === 0) {
1100
+ // Nothing old enough to summarize (conversation already within the
1101
+ // keep-recent budget). Report a no-op rather than summarizing nothing.
1102
+ const postTokens = this.estimateFullContextTokens();
1103
+ this.lastContextTokens = postTokens;
1104
+ this.emit({
1105
+ type: "timeline",
1106
+ provider: this.provider,
1107
+ turnId: turn.turnId,
1108
+ item: { type: "compaction", status: "completed", trigger, preTokens, postTokens },
1109
+ });
1110
+ const maxTokens = await this.resolveContextWindowMaxTokens();
1111
+ const usage = {
1112
+ contextWindowUsedTokens: postTokens,
1113
+ ...(maxTokens !== null ? { contextWindowMaxTokens: maxTokens } : {}),
1114
+ };
1115
+ this.emit({ type: "usage_updated", provider: this.provider, usage, turnId: turn.turnId });
1116
+ return usage;
1117
+ }
1118
+ // 3. Incremental update when a prior summary sits in the region: feed it as
1119
+ // <previous-summary> and merge the newer messages into it, rather than
1120
+ // summarizing a summary (which degrades badly across repeated compacts).
1121
+ const priorSummaryIndex = summarizeRegion.findIndex((message) => message.role === "user" && message.isCompactionSummary === true);
1122
+ const priorSummary = priorSummaryIndex >= 0 ? summarizeRegion[priorSummaryIndex].content : null;
1123
+ const messagesToSummarize = priorSummaryIndex >= 0
1124
+ ? summarizeRegion.filter((_, index) => index !== priorSummaryIndex)
1125
+ : summarizeRegion;
1126
+ const conversationText = this.serializeConversationForCompaction(messagesToSummarize);
1127
+ const endpoint = resolveEndpoint(this.env, this.label);
1128
+ const model = await this.resolveModel(endpoint);
1129
+ const systemPrompt = priorSummary !== null
1130
+ ? this.buildCompactionUpdateSystemPrompt(instruction)
1131
+ : this.buildCompactionSystemPrompt(instruction);
1132
+ const userContent = priorSummary !== null
1133
+ ? `<previous-summary>\n${priorSummary}\n</previous-summary>\n\n<new-messages>\n${conversationText}\n</new-messages>`
1134
+ : conversationText;
1135
+ const summaryResult = await this.runCompactionCompletion({
1136
+ endpoint,
1137
+ model,
1138
+ systemPrompt,
1139
+ userContent,
1140
+ signal: turn.abort.signal,
1141
+ });
1142
+ const summary = summaryResult.content;
1143
+ // 4. Rebuild: system + summary (marked for future incremental updates) +
1144
+ // the retained recent tail, untouched.
1145
+ this.messages.length = 0;
1146
+ this.messages.push({ role: "system", content: this.buildSystemPrompt(this.config) });
1147
+ this.messages.push({
1148
+ role: "user",
1149
+ content: summary,
1150
+ messageId: randomUUID(),
1151
+ isCompactionSummary: true,
1152
+ });
1153
+ // Assistant ack keeps the roles alternating so the next user turn doesn't
1154
+ // sit directly after the synthetic summary user message.
1155
+ this.messages.push({ role: "assistant", content: "Conversation history has been compacted." });
1156
+ this.messages.push(...keptRegion);
1157
+ // Post-compaction context size. Must count the rebuilt system prompt and
1158
+ // the tool schemas — both are re-sent with every request — or the ring
1159
+ // reads near-zero here and jumps back up on the next real turn.
1160
+ const postTokens = this.estimateFullContextTokens();
1161
+ this.lastContextTokens = postTokens;
1162
+ this.emit({
1163
+ type: "timeline",
1164
+ provider: this.provider,
1165
+ turnId: turn.turnId,
1166
+ item: {
1167
+ type: "compaction",
1168
+ status: "completed",
1169
+ trigger,
1170
+ preTokens,
1171
+ postTokens,
1172
+ },
1173
+ });
1174
+ // usage_updated replaces agent.lastUsage wholesale and the client hides
1175
+ // the context ring unless both bounds are present, so the compaction
1176
+ // usage must carry the post-compaction context size alongside the
1177
+ // request's own token cost.
1178
+ const maxTokens = await this.resolveContextWindowMaxTokens();
1179
+ const usageData = summaryResult.usage;
1180
+ const usage = {
1181
+ ...(typeof usageData?.prompt_tokens === "number"
1182
+ ? { inputTokens: usageData.prompt_tokens }
1183
+ : {}),
1184
+ ...(typeof usageData?.completion_tokens === "number"
1185
+ ? { outputTokens: usageData.completion_tokens }
1186
+ : {}),
1187
+ contextWindowUsedTokens: postTokens,
1188
+ ...(maxTokens !== null ? { contextWindowMaxTokens: maxTokens } : {}),
1189
+ };
1190
+ this.emit({
1191
+ type: "usage_updated",
1192
+ provider: this.provider,
1193
+ usage,
1194
+ turnId: turn.turnId,
1195
+ });
1196
+ return usage;
1197
+ }
1198
+ /**
1199
+ * Auto-compaction check, run at turn start (before the new user message
1200
+ * joins the conversation, so it always survives verbatim) and before every
1201
+ * subsequent model round. Uses the freshest server-measured context size —
1202
+ * the active turn's last round, falling back to the previous turn's final
1203
+ * figure. Endpoints that report no context length never auto-compact:
1204
+ * without a denominator there is no percentage (manual /compact still
1205
+ * works).
1206
+ *
1207
+ * Loop protection: a compaction that fails or can't bring usage back under
1208
+ * the threshold disarms the trigger. Without that, a conversation whose
1209
+ * retained tail alone exceeds the threshold would re-summarize on every
1210
+ * round, burning a model call each time for no reclaimed space. The trigger
1211
+ * re-arms once measured usage drops below the threshold by other means
1212
+ * (rewind, manual /compact, a model switch to a larger window) or when the
1213
+ * user changes the auto-compact setting.
1214
+ *
1215
+ * Auto-compaction failures never fail the user's turn — the turn proceeds
1216
+ * with the uncompacted conversation (interrupts still propagate).
1217
+ */
1218
+ async maybeAutoCompact(turn) {
1219
+ if (this.autoCompact === "off") {
1220
+ return;
1221
+ }
1222
+ const maxTokens = await this.resolveContextWindowMaxTokens();
1223
+ if (maxTokens === null) {
1224
+ return;
1225
+ }
1226
+ const threshold = Math.floor((Number(this.autoCompact) / 100) * maxTokens);
1227
+ const used = turn.usage && typeof turn.usage.inputTokens === "number"
1228
+ ? turn.usage.inputTokens + (turn.usage.outputTokens ?? 0)
1229
+ : this.lastContextTokens;
1230
+ if (used === null || used < threshold) {
1231
+ if (used !== null) {
1232
+ this.autoCompactDisarmed = false;
1233
+ }
1234
+ return;
1235
+ }
1236
+ if (this.autoCompactDisarmed) {
1237
+ return;
1238
+ }
1239
+ let usage;
1240
+ try {
1241
+ usage = await this.handleCompact(turn, null, "auto");
1242
+ }
1243
+ catch (error) {
1244
+ // Settle the "loading" compaction row before deciding how to proceed.
1245
+ this.emit({
1246
+ type: "timeline",
1247
+ provider: this.provider,
1248
+ turnId: turn.turnId,
1249
+ item: { type: "compaction", status: "failed", trigger: "auto" },
1250
+ });
1251
+ if (turn.abort.signal.aborted) {
1252
+ throw error;
1253
+ }
1254
+ this.autoCompactDisarmed = true;
1255
+ const message = error instanceof Error ? error.message : String(error);
1256
+ this.emit({
1257
+ type: "timeline",
1258
+ provider: this.provider,
1259
+ turnId: turn.turnId,
1260
+ item: {
1261
+ type: "error",
1262
+ message: `Auto-compaction failed: ${message}. Auto-compaction is paused until context usage drops below the threshold — run /compact to retry manually.`,
1263
+ },
1264
+ });
1265
+ return;
1266
+ }
1267
+ // The pre-compaction round figures no longer describe the conversation;
1268
+ // the next round's stream re-measures it.
1269
+ turn.usage = null;
1270
+ const postTokens = usage.contextWindowUsedTokens ?? this.lastContextTokens;
1271
+ if (typeof postTokens === "number" && postTokens >= threshold) {
1272
+ this.autoCompactDisarmed = true;
1273
+ this.emit({
1274
+ type: "timeline",
1275
+ provider: this.provider,
1276
+ turnId: turn.turnId,
1277
+ item: {
1278
+ type: "error",
1279
+ message: `Auto-compaction reclaimed too little space (still ~${postTokens} of ` +
1280
+ `${maxTokens} tokens). Auto-compaction is paused until usage ` +
1281
+ `drops below the ${this.autoCompact}% threshold — run /compact with an ` +
1282
+ `instruction, rewind, or start a fresh agent.`,
1283
+ },
1284
+ });
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Pre-summarization pruning (zero-LLM). Mutates the message array in place:
1289
+ * elides uneventful tool results everywhere, then truncates oversized tool
1290
+ * results older than the protected-recent window to head + tail. Structure is
1291
+ * never changed (only tool `content`), so tool_call ordering stays valid.
1292
+ */
1293
+ pruneToolOutputs() {
1294
+ for (const message of this.messages) {
1295
+ if (message.role === "tool" && isUneventfulToolResult(message.content)) {
1296
+ message.content = UNEVENTFUL_RESULT_PLACEHOLDER;
1297
+ }
1298
+ }
1299
+ // Walk tool results newest → oldest; protect the freshest by token budget,
1300
+ // truncate the rest.
1301
+ let protectedTokens = 0;
1302
+ for (let index = this.messages.length - 1; index >= 0; index -= 1) {
1303
+ const message = this.messages[index];
1304
+ if (message.role !== "tool") {
1305
+ continue;
1306
+ }
1307
+ const tokens = estimateTokens(message.content);
1308
+ if (protectedTokens < PRUNE_PROTECT_RECENT_TOOL_TOKENS) {
1309
+ protectedTokens += tokens;
1310
+ continue;
1311
+ }
1312
+ if (message.content.length > PRUNE_TOOL_RESULT_MIN_CHARS) {
1313
+ const { content } = message;
1314
+ message.content = `${content.slice(0, PRUNE_TOOL_RESULT_HEAD_CHARS)}\n[... ${content.length - PRUNE_TOOL_RESULT_HEAD_CHARS - PRUNE_TOOL_RESULT_TAIL_CHARS} chars pruned ...]\n${content.slice(-PRUNE_TOOL_RESULT_TAIL_CHARS)}`;
1315
+ }
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Index of the first message (after the system prompt at 0) to keep verbatim
1320
+ * through compaction: walk newest → oldest accumulating token estimates until
1321
+ * the keep-recent budget is exceeded, then advance past any leading tool
1322
+ * results so the retained tail never starts with a tool message orphaned from
1323
+ * its assistant tool_calls. Returns messages.length when the whole
1324
+ * conversation is older than the budget (summarize everything, keep none).
1325
+ */
1326
+ computeCompactionKeepFromIndex() {
1327
+ let keepFrom = this.messages.length;
1328
+ let recentTokens = 0;
1329
+ for (let index = this.messages.length - 1; index >= 1; index -= 1) {
1330
+ recentTokens += estimateTokens(JSON.stringify(this.messages[index]));
1331
+ if (recentTokens > this.keepRecentTokens) {
1332
+ break;
1333
+ }
1334
+ keepFrom = index;
1335
+ }
1336
+ // Never keep the entire conversation: a manual /compact should always
1337
+ // compress something, so a conversation within the budget summarizes fully.
1338
+ if (keepFrom <= 1) {
1339
+ return this.messages.length;
1340
+ }
1341
+ // Don't strand tool results at the head of the retained tail.
1342
+ while (keepFrom < this.messages.length && this.messages[keepFrom].role === "tool") {
1343
+ keepFrom += 1;
1344
+ }
1345
+ return keepFrom;
1346
+ }
1347
+ /**
1348
+ * POST a non-streaming chat completion for compaction and return the assistant
1349
+ * text plus raw usage. Throws on transport failure or an empty response.
1350
+ */
1351
+ async runCompactionCompletion(options) {
1352
+ const response = await fetch(`${options.endpoint.baseUrl}/chat/completions`, {
1353
+ method: "POST",
1354
+ headers: buildHeaders(options.endpoint),
1355
+ signal: options.signal,
1356
+ body: JSON.stringify({
1357
+ model: options.model,
1358
+ messages: [
1359
+ { role: "system", content: options.systemPrompt },
1360
+ { role: "user", content: options.userContent },
1361
+ ],
1362
+ }),
1363
+ });
1364
+ if (!response.ok) {
1365
+ const bodyText = await response.text().catch(() => "");
1366
+ throw new Error(`${this.label} responded ${response.status} to /chat/completions${bodyText ? `: ${bodyText.slice(0, 400)}` : ""}`);
1367
+ }
1368
+ const data = (await response.json());
1369
+ const choices = Array.isArray(data.choices) ? data.choices : [];
1370
+ const firstChoice = choices[0];
1371
+ if (!firstChoice || typeof firstChoice !== "object") {
1372
+ throw new Error(`${this.label} returned an empty compaction response`);
1373
+ }
1374
+ const responseMessage = firstChoice.message;
1375
+ if (!responseMessage || typeof responseMessage !== "object") {
1376
+ throw new Error(`${this.label} returned an empty compaction response`);
1377
+ }
1378
+ const rawContent = responseMessage.content;
1379
+ const content = typeof rawContent === "string" ? rawContent : "";
1380
+ if (!content) {
1381
+ throw new Error(`${this.label} returned an empty compaction summary`);
1382
+ }
1383
+ const usage = data.usage && typeof data.usage === "object"
1384
+ ? data.usage
1385
+ : undefined;
1386
+ return { content, ...(usage ? { usage } : {}) };
1387
+ }
1388
+ /**
1389
+ * Local estimate of the full next-request context: conversation messages
1390
+ * (including the system prompt) plus the tool schemas that are re-sent with
1391
+ * every request. Char-based, so it's approximate — the next real turn's
1392
+ * server-measured usage replaces it.
1393
+ */
1394
+ estimateFullContextTokens() {
1395
+ const toolsPayload = [
1396
+ ...buildOpenAIToolsPayload(this.availableToolSpecs()),
1397
+ ...this.buildOttoToolPayload(),
1398
+ ...this.buildMcpToolPayload(),
1399
+ ];
1400
+ return (estimateTokens(JSON.stringify(toolsPayload)) +
1401
+ this.messages.reduce((sum, message) => sum + estimateTokens(JSON.stringify(message)), 0));
1402
+ }
1403
+ /**
1404
+ * Serialize the given conversation messages into a text blob suitable for a
1405
+ * compaction prompt. Tool calls are summarized rather than included verbatim
1406
+ * to keep the payload manageable.
1407
+ */
1408
+ serializeConversationForCompaction(messages) {
1409
+ const lines = [];
1410
+ const conversationMessages = messages.filter((message) => message.role !== "system");
1411
+ for (const message of conversationMessages) {
1412
+ if (message.role === "user") {
1413
+ lines.push(`[user]: ${message.content}`);
1414
+ }
1415
+ else if (message.role === "assistant") {
1416
+ if (message.tool_calls && message.tool_calls.length > 0) {
1417
+ const toolSummaries = message.tool_calls
1418
+ .map((call) => {
1419
+ let argsPreview = "";
1420
+ try {
1421
+ const parsed = JSON.parse(call.function.arguments);
1422
+ argsPreview = ` (${JSON.stringify(parsed).slice(0, 500)})`;
1423
+ }
1424
+ catch {
1425
+ argsPreview = ` (${call.function.arguments.slice(0, 500)})`;
1426
+ }
1427
+ return ` - called ${call.function.name}${argsPreview}`;
1428
+ })
1429
+ .join("\n");
1430
+ lines.push(`[assistant]: (tool calls)`);
1431
+ lines.push(toolSummaries);
1432
+ if (message.content) {
1433
+ lines.push(` text: ${message.content}`);
1434
+ }
1435
+ }
1436
+ else {
1437
+ lines.push(`[assistant]: ${message.content}`);
1438
+ }
1439
+ }
1440
+ else if (message.role === "tool") {
1441
+ // Keep head + tail of large results: file reads and command output
1442
+ // carry their signal at both ends, and the summarizer can't preserve
1443
+ // what it never sees.
1444
+ const content = message.content;
1445
+ const contentPreview = content.length > TOOL_RESULT_HEAD_CHARS + TOOL_RESULT_TAIL_CHARS
1446
+ ? `${content.slice(0, TOOL_RESULT_HEAD_CHARS)}\n[... ${content.length - TOOL_RESULT_HEAD_CHARS - TOOL_RESULT_TAIL_CHARS} chars truncated ...]\n${content.slice(-TOOL_RESULT_TAIL_CHARS)}`
1447
+ : content;
1448
+ lines.push(`[tool result ${message.tool_call_id}]: ${contentPreview}`);
1449
+ }
1450
+ }
1451
+ return lines.join("\n");
1452
+ }
1453
+ /**
1454
+ * System prompt for a fresh compaction summary. The older conversation is
1455
+ * summarized while recent messages are kept verbatim, so this summary becomes
1456
+ * the model's memory of the distant past — it must preserve exact references,
1457
+ * not paraphrase them away. Modeled on oh-my-pi's structured handoff format.
1458
+ */
1459
+ buildCompactionSystemPrompt(instruction) {
1460
+ const base = `You summarize a coding-agent conversation into a structured handoff summary so another LLM can resume the task. Only the older part of the conversation is being summarized; recent messages are retained verbatim after your summary. Preserve detail — a thorough summary is expected. Do NOT aim for brevity.
1461
+
1462
+ NEVER continue the conversation. NEVER answer questions found in it. Output ONLY the structured summary below.
1463
+
1464
+ If the conversation ends with an unanswered question or a request awaiting a response (e.g. "run this and paste the output"), you MUST preserve that exact question/request under Critical Context.
1465
+
1466
+ Use this format (omit a section only if it truly does not apply):
1467
+
1468
+ ## Goal
1469
+ [The user's goals; list multiple if the session covers different tasks.]
1470
+
1471
+ ## Constraints & Preferences
1472
+ - [Requirements, constraints, and preferences the user stated]
1473
+
1474
+ ## Progress
1475
+ ### Done
1476
+ - [x] [Completed changes/tasks]
1477
+ ### In Progress
1478
+ - [ ] [Work underway when compaction ran]
1479
+ ### Blocked
1480
+ - [Anything preventing progress]
1481
+
1482
+ ## Key Decisions
1483
+ - **[Decision]**: [Brief rationale]
1484
+
1485
+ ## Files & Changes
1486
+ - [Exact path — what was read/created/modified and why it matters; include the important code snippets verbatim]
1487
+
1488
+ ## Next Steps
1489
+ 1. [Ordered next actions, aligned with the user's most recent request]
1490
+
1491
+ ## Critical Context
1492
+ - [Important data, exact error messages, repository state (branch, uncommitted changes), and any pending question awaiting a user response]
1493
+
1494
+ ## Additional Notes
1495
+ [Anything important not captured above]
1496
+
1497
+ You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results.`;
1498
+ const instructionPart = instruction
1499
+ ? `\n\nAdditional user instruction — follow it for emphasis and focus, on top of the format above:\n<user-instruction>${instruction}</user-instruction>`
1500
+ : "";
1501
+ return base + instructionPart;
1502
+ }
1503
+ /**
1504
+ * System prompt for an incremental compaction: merge newer messages into an
1505
+ * existing summary rather than re-summarizing a summary (which degrades fast
1506
+ * across repeated compacts). The prior summary arrives in <previous-summary>
1507
+ * tags and the newer messages in <new-messages> tags.
1508
+ */
1509
+ buildCompactionUpdateSystemPrompt(instruction) {
1510
+ const base = `You update an existing structured handoff summary by incorporating newer conversation messages, so another LLM can resume the task. The prior summary is in <previous-summary> tags; the newer messages are in <new-messages> tags.
1511
+
1512
+ NEVER continue the conversation. NEVER answer questions found in it. Output ONLY the updated structured summary.
1513
+
1514
+ Rules:
1515
+ - Preserve ALL information from the previous summary that is still relevant.
1516
+ - Fold in new progress, decisions, files, and context from the newer messages.
1517
+ - Move items from "In Progress" to "Done" when completed; update "Next Steps".
1518
+ - You MAY drop anything the newer messages made irrelevant.
1519
+ - If the newer messages end with an unanswered question or a request awaiting a response, record it under Critical Context (replacing a prior pending question once answered).
1520
+ - You MUST preserve exact file paths, function names, error messages, and relevant tool outputs or command results.
1521
+
1522
+ Keep the same section format as the previous summary (## Goal, ## Constraints & Preferences, ## Progress with Done/In Progress/Blocked, ## Key Decisions, ## Files & Changes, ## Next Steps, ## Critical Context, ## Additional Notes).`;
1523
+ const instructionPart = instruction
1524
+ ? `\n\nAdditional user instruction — follow it for emphasis and focus:\n<user-instruction>${instruction}</user-instruction>`
1525
+ : "";
1526
+ return base + instructionPart;
1527
+ }
1528
+ /**
1529
+ * When the typed prompt is `/mcp_{server}_{prompt} args`, resolve it via
1530
+ * prompts/get and feed the resolved text to the model. The remainder of the
1531
+ * line maps to the prompt's first declared argument (multi-argument prompts
1532
+ * receive only that one). Any resolution failure falls back to sending the
1533
+ * raw text as a plain prompt, matching the opencode provider.
1534
+ */
1535
+ async resolveMcpPromptText(promptText) {
1536
+ const manager = this.mcpManager;
1537
+ if (!manager) {
1538
+ return promptText;
1539
+ }
1540
+ const parsed = parseSlashCommandInput(promptText);
1541
+ if (!parsed || !parsed.commandName.startsWith("mcp_")) {
1542
+ return promptText;
1543
+ }
1544
+ try {
1545
+ const prompts = await manager.listPrompts();
1546
+ const binding = prompts.find((prompt) => prompt.commandName === parsed.commandName);
1547
+ if (!binding) {
1548
+ return promptText;
1549
+ }
1550
+ return await manager.getPrompt(binding, parsed.args);
1551
+ }
1552
+ catch (error) {
1553
+ this.logger?.warn({ err: error, commandName: parsed.commandName }, "Failed to resolve MCP prompt command; falling back to plain prompt input");
1554
+ return promptText;
1555
+ }
1556
+ }
1557
+ /**
1558
+ * MCP tools rendered as OpenAI function specs under namespaced
1559
+ * mcp_{server}_{tool} names. Hard-excluded in read-only "plan" mode — MCP
1560
+ * tools are opaque and may take actions regardless of what they claim.
1561
+ */
1562
+ buildMcpToolPayload() {
1563
+ if (!this.mcpManager || this.modeId === "plan") {
1564
+ return [];
1565
+ }
1566
+ return this.mcpManager.getToolBindings().map((binding) => ({
1567
+ type: "function",
1568
+ function: {
1569
+ name: binding.modelName,
1570
+ description: binding.description,
1571
+ parameters: binding.parameters,
1572
+ },
1573
+ }));
1574
+ }
1575
+ buildOttoToolPayload() {
1576
+ if (!this.ottoTools || this.modeId === "plan") {
1577
+ return [];
1578
+ }
1579
+ const payload = [];
1580
+ for (const tool of this.ottoTools.tools.values()) {
1581
+ if (findCompatToolSpec(tool.name) || !this.isOttoToolGroupEnabled(tool.name)) {
1582
+ continue;
1583
+ }
1584
+ payload.push({
1585
+ type: "function",
1586
+ function: {
1587
+ name: tool.name,
1588
+ description: tool.description,
1589
+ parameters: ottoToolParameters(tool),
1590
+ },
1591
+ });
1592
+ }
1593
+ return payload;
1594
+ }
1595
+ async run(prompt, options) {
1596
+ const { turnId } = await this.startTurn(prompt, options);
1597
+ const turn = this.activeTurn;
1598
+ if (!turn || turn.turnId !== turnId) {
1599
+ throw new Error(`${this.label} turn did not start`);
1600
+ }
1601
+ return turn.completed;
1602
+ }
1603
+ async startTurn(prompt, options) {
1604
+ if (this.activeTurn) {
1605
+ throw new Error(`${this.label} already has an active turn`);
1606
+ }
1607
+ const turnId = randomUUID();
1608
+ let resolve;
1609
+ let reject;
1610
+ const completed = new Promise((promiseResolve, promiseReject) => {
1611
+ resolve = promiseResolve;
1612
+ reject = promiseReject;
1613
+ });
1614
+ // startTurn callers observe failure via turn_failed events, not this
1615
+ // promise — swallow to avoid unhandled rejections. run() awaits the same
1616
+ // promise and still sees the rejection.
1617
+ completed.catch(() => { });
1618
+ const turn = {
1619
+ turnId,
1620
+ assistantMessageId: randomUUID(),
1621
+ abort: new AbortController(),
1622
+ roundText: "",
1623
+ roundReasoning: "",
1624
+ finalTextParts: [],
1625
+ pendingToolCalls: new Map(),
1626
+ finishReason: null,
1627
+ usage: null,
1628
+ resolve,
1629
+ reject,
1630
+ completed,
1631
+ };
1632
+ this.activeTurn = turn;
1633
+ const promptText = promptToText(prompt);
1634
+ // One id shared by the timeline item and the conversation entry so
1635
+ // revertConversation can map a timeline user_message back to its message.
1636
+ const userMessageId = options?.messageId ?? randomUUID();
1637
+ this.emit({
1638
+ type: "timeline",
1639
+ provider: this.provider,
1640
+ turnId,
1641
+ item: {
1642
+ type: "user_message",
1643
+ text: promptText,
1644
+ messageId: userMessageId,
1645
+ },
1646
+ });
1647
+ void this.executeTurn(turn, promptText, userMessageId);
1648
+ return { turnId };
1649
+ }
1650
+ async executeTurn(turn, promptText, userMessageId) {
1651
+ this.emit({ type: "turn_started", provider: this.provider, turnId: turn.turnId });
1652
+ // Intercept built-in slash commands before delegating to the model.
1653
+ const slashCommand = parseSlashCommandInput(promptText);
1654
+ if (slashCommand && slashCommand.commandName === "compact") {
1655
+ let usage;
1656
+ try {
1657
+ usage = await this.handleCompact(turn, slashCommand.args, "manual");
1658
+ }
1659
+ catch (error) {
1660
+ // Settle the "loading" compaction row — without this it spins
1661
+ // forever. Clients that predate the "failed" status drop this event
1662
+ // and keep the spinning row, which matches their pre-fix behavior.
1663
+ this.emit({
1664
+ type: "timeline",
1665
+ provider: this.provider,
1666
+ turnId: turn.turnId,
1667
+ item: { type: "compaction", status: "failed", trigger: "manual" },
1668
+ });
1669
+ this.settleTurnFailure(turn, error);
1670
+ return;
1671
+ }
1672
+ if (this.activeTurn !== turn) {
1673
+ return;
1674
+ }
1675
+ this.activeTurn = null;
1676
+ // turn_completed is the manager's terminal signal — without it the
1677
+ // foreground turn stream never ends and the agent stays "running".
1678
+ this.emit({
1679
+ type: "turn_completed",
1680
+ provider: this.provider,
1681
+ turnId: turn.turnId,
1682
+ usage,
1683
+ });
1684
+ turn.resolve({
1685
+ sessionId: this.id,
1686
+ finalText: "",
1687
+ usage,
1688
+ timeline: [],
1689
+ canceled: false,
1690
+ });
1691
+ return;
1692
+ }
1693
+ try {
1694
+ await this.ensureMcpReady(turn);
1695
+ // The timeline keeps the typed `/command args` text; the model receives
1696
+ // the resolved MCP prompt (or the raw text when nothing matches).
1697
+ const modelText = await this.resolveMcpPromptText(promptText);
1698
+ // Turn-start auto-compaction runs before the new user message joins the
1699
+ // conversation: when the whole history is summarized ("never keep the
1700
+ // entire conversation"), the instruction the model is about to act on
1701
+ // must not be folded into the summary with it.
1702
+ await this.maybeAutoCompact(turn);
1703
+ this.messages.push({ role: "user", content: modelText, messageId: userMessageId });
1704
+ await this.runToolLoop(turn);
1705
+ }
1706
+ catch (error) {
1707
+ this.settleTurnFailure(turn, error);
1708
+ return;
1709
+ }
1710
+ if (this.activeTurn !== turn) {
1711
+ return;
1712
+ }
1713
+ this.activeTurn = null;
1714
+ const finalText = turn.finalTextParts.join("\n\n");
1715
+ const usage = await this.buildTurnUsage(turn);
1716
+ this.emit({
1717
+ type: "turn_completed",
1718
+ provider: this.provider,
1719
+ turnId: turn.turnId,
1720
+ ...(usage ? { usage } : {}),
1721
+ });
1722
+ turn.resolve({
1723
+ sessionId: this.id,
1724
+ finalText,
1725
+ ...(usage ? { usage } : {}),
1726
+ timeline: [],
1727
+ canceled: false,
1728
+ });
1729
+ }
1730
+ async runToolLoop(turn) {
1731
+ // Warm the context-window cache before streaming: emitStreamUsageUpdated
1732
+ // runs on the hot chunk path and can only read the cached value, and a
1733
+ // usage_updated without contextWindowMaxTokens blanks the client's
1734
+ // context ring (agent.lastUsage is replaced wholesale, and the ring
1735
+ // needs both bounds). Cached per model, so this is one probe per session.
1736
+ await this.resolveContextWindowMaxTokens();
1737
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
1738
+ // Round 0 was already checked at turn start; re-check between tool
1739
+ // rounds so a long tool loop compacts mid-turn instead of overflowing.
1740
+ if (round > 0) {
1741
+ await this.maybeAutoCompact(turn);
1742
+ }
1743
+ await this.streamCompletion(turn);
1744
+ const toolCalls = [...turn.pendingToolCalls.entries()]
1745
+ .sort(([a], [b]) => a - b)
1746
+ .map(([, call]) => call);
1747
+ if (turn.roundText || turn.roundReasoning || toolCalls.length > 0) {
1748
+ this.messages.push({
1749
+ role: "assistant",
1750
+ content: turn.roundText,
1751
+ ...(turn.roundReasoning ? { reasoning: turn.roundReasoning } : {}),
1752
+ ...(toolCalls.length > 0
1753
+ ? {
1754
+ tool_calls: toolCalls.map((call) => ({
1755
+ id: call.id,
1756
+ type: "function",
1757
+ function: { name: call.name, arguments: call.argumentsJson },
1758
+ })),
1759
+ }
1760
+ : {}),
1761
+ });
1762
+ }
1763
+ if (turn.roundText) {
1764
+ turn.finalTextParts.push(turn.roundText);
1765
+ }
1766
+ // Consumed — settleTurnFailure must not re-append these if a later
1767
+ // tool round gets interrupted.
1768
+ turn.roundText = "";
1769
+ turn.roundReasoning = "";
1770
+ if (toolCalls.length === 0) {
1771
+ return;
1772
+ }
1773
+ for (const call of toolCalls) {
1774
+ if (turn.abort.signal.aborted) {
1775
+ throw new Error("Interrupted");
1776
+ }
1777
+ const resultContent = await this.executeToolCall(turn, call);
1778
+ this.messages.push({ role: "tool", content: resultContent, tool_call_id: call.id });
1779
+ }
1780
+ if (turn.abort.signal.aborted) {
1781
+ throw new Error("Interrupted");
1782
+ }
1783
+ }
1784
+ this.emit({
1785
+ type: "timeline",
1786
+ provider: this.provider,
1787
+ turnId: turn.turnId,
1788
+ item: {
1789
+ type: "error",
1790
+ message: `Stopped after ${MAX_TOOL_ROUNDS} tool rounds without a final answer.`,
1791
+ },
1792
+ });
1793
+ }
1794
+ parseToolCallArgs(turn, call) {
1795
+ try {
1796
+ const parsed = call.argumentsJson ? JSON.parse(call.argumentsJson) : {};
1797
+ return {
1798
+ args: parsed && typeof parsed === "object" ? parsed : {},
1799
+ };
1800
+ }
1801
+ catch {
1802
+ this.emitToolItem(turn, call, "failed", buildCompatToolPreviewDetail(call.name, {}, this.cwd), "Invalid tool arguments (malformed JSON)");
1803
+ return { error: `Error: arguments for ${call.name} were not valid JSON` };
1804
+ }
1805
+ }
1806
+ async executeToolCall(turn, call) {
1807
+ const spec = findCompatToolSpec(call.name);
1808
+ const parsed = this.parseToolCallArgs(turn, call);
1809
+ if ("error" in parsed) {
1810
+ return parsed.error;
1811
+ }
1812
+ const args = parsed.args;
1813
+ // Otto catalog tools are dispatched to the catalog, not the builtin coding
1814
+ // tools. Builtins take precedence on name; plan mode offers neither actions
1815
+ // nor Otto tools, so a plan-mode call for one falls through to "not available".
1816
+ if (!spec && this.modeId !== "plan") {
1817
+ const ottoTool = this.isOttoToolGroupEnabled(call.name)
1818
+ ? this.ottoTools?.getTool(call.name)
1819
+ : undefined;
1820
+ if (ottoTool) {
1821
+ return this.executeOttoToolCall(turn, call, args, ottoTool);
1822
+ }
1823
+ // MCP tools live under namespaced mcp_{server}_{tool} names, so they can
1824
+ // never collide with (or shadow) builtins or Otto tools.
1825
+ const mcpBinding = this.mcpManager?.resolveTool(call.name);
1826
+ if (mcpBinding) {
1827
+ return this.executeMcpToolCall(turn, call, args, mcpBinding);
1828
+ }
1829
+ }
1830
+ const previewDetail = buildCompatToolPreviewDetail(call.name, args, this.cwd);
1831
+ if (!spec || !this.availableToolSpecs().some((candidate) => candidate.name === spec.name)) {
1832
+ this.emitToolItem(turn, call, "failed", previewDetail, `Tool ${call.name} is not available`);
1833
+ return `Error: tool ${call.name} is not available in the current mode`;
1834
+ }
1835
+ if (this.toolNeedsApproval(spec, args)) {
1836
+ const response = await this.requestPermission(turn, {
1837
+ name: spec.name,
1838
+ title: spec.name,
1839
+ description: COMPAT_TOOL_PROMPT_DESCRIPTIONS[spec.kind],
1840
+ args,
1841
+ detail: previewDetail,
1842
+ });
1843
+ if (response.behavior === "deny") {
1844
+ this.emitToolItem(turn, call, "failed", previewDetail, "Denied by user");
1845
+ if (response.interrupt) {
1846
+ turn.abort.abort();
1847
+ }
1848
+ const message = response.message?.trim();
1849
+ return message
1850
+ ? `The user declined this tool call: ${message}`
1851
+ : "The user declined this tool call.";
1852
+ }
1853
+ }
1854
+ this.emitToolItem(turn, call, "running", previewDetail, null);
1855
+ const outcome = await executeCompatTool({
1856
+ name: call.name,
1857
+ arguments: args,
1858
+ cwd: this.cwd,
1859
+ signal: turn.abort.signal,
1860
+ });
1861
+ if (turn.abort.signal.aborted) {
1862
+ this.emitToolItem(turn, call, "canceled", outcome.detail, null);
1863
+ return outcome.output;
1864
+ }
1865
+ this.emitToolItem(turn, call, outcome.isError ? "failed" : "completed", outcome.detail, outcome.isError ? outcome.output : null);
1866
+ return outcome.output;
1867
+ }
1868
+ /**
1869
+ * MCP tools are opaque — the daemon cannot know whether one is destructive.
1870
+ * default mode always asks; acceptEdits asks unless the provider opted into
1871
+ * "trust-read-only" AND the server self-declares readOnlyHint (that
1872
+ * annotation is untrusted, so it never skips prompts in default mode);
1873
+ * bypassPermissions auto-approves like everything else. Plan mode never
1874
+ * reaches here — MCP tools are excluded from the payload entirely.
1875
+ */
1876
+ mcpToolNeedsApproval(binding) {
1877
+ if (this.modeId === "bypassPermissions")
1878
+ return false;
1879
+ if (this.modeId === "acceptEdits") {
1880
+ return !(this.mcpToolPermissions === "trust-read-only" && binding.readOnlyHint);
1881
+ }
1882
+ return true;
1883
+ }
1884
+ async executeMcpToolCall(turn, call, args, binding) {
1885
+ const manager = this.mcpManager;
1886
+ if (!manager) {
1887
+ return `Error: tool ${call.name} is not available`;
1888
+ }
1889
+ const detail = { type: "unknown", input: args, output: null };
1890
+ if (this.mcpToolNeedsApproval(binding)) {
1891
+ const response = await this.requestPermission(turn, {
1892
+ name: binding.modelName,
1893
+ title: `${binding.serverName}: ${binding.toolName}`,
1894
+ description: `Wants to run an MCP tool from server '${binding.serverName}'`,
1895
+ args,
1896
+ detail,
1897
+ });
1898
+ if (response.behavior === "deny") {
1899
+ this.emitToolItem(turn, call, "failed", detail, "Denied by user");
1900
+ if (response.interrupt) {
1901
+ turn.abort.abort();
1902
+ }
1903
+ const message = response.message?.trim();
1904
+ return message
1905
+ ? `The user declined this tool call: ${message}`
1906
+ : "The user declined this tool call.";
1907
+ }
1908
+ }
1909
+ this.emitToolItem(turn, call, "running", detail, null);
1910
+ const outcome = await manager.callTool(binding, args, { signal: turn.abort.signal });
1911
+ if (turn.abort.signal.aborted) {
1912
+ this.emitToolItem(turn, call, "canceled", { ...detail, output: outcome.output }, null);
1913
+ return outcome.output;
1914
+ }
1915
+ this.emitToolItem(turn, call, outcome.isError ? "failed" : "completed", { ...detail, output: outcome.output }, outcome.isError ? outcome.output : null);
1916
+ return outcome.output;
1917
+ }
1918
+ /**
1919
+ * Otto tools skip prompts only when classified read-only; "interact" tools
1920
+ * are auto-approved in acceptEdits like file edits, and "execute" tools
1921
+ * always prompt outside bypassPermissions. CLI providers get this gating
1922
+ * from their own permission system in front of the MCP client — here the
1923
+ * daemon is the runtime, so it prompts itself.
1924
+ * See openai-compat-otto-tool-permissions.ts.
1925
+ */
1926
+ ottoToolNeedsApproval(name) {
1927
+ const kind = ottoToolPermissionKind(name);
1928
+ if (kind === "read")
1929
+ return false;
1930
+ if (this.modeId === "bypassPermissions")
1931
+ return false;
1932
+ if (this.modeId === "acceptEdits")
1933
+ return kind === "execute";
1934
+ return true;
1935
+ }
1936
+ async executeOttoToolCall(turn, call, args, tool) {
1937
+ const catalog = this.ottoTools;
1938
+ if (!catalog) {
1939
+ return `Error: tool ${call.name} is not available`;
1940
+ }
1941
+ const detail = { type: "unknown", input: args, output: null };
1942
+ if (this.ottoToolNeedsApproval(tool.name)) {
1943
+ const response = await this.requestPermission(turn, {
1944
+ name: tool.name,
1945
+ title: tool.name,
1946
+ description: ottoToolPermissionKind(tool.name) === "execute"
1947
+ ? "Wants to run an Otto tool that can execute code or manage agents"
1948
+ : "Wants to interact with the Otto browser or preview servers",
1949
+ args,
1950
+ detail,
1951
+ });
1952
+ if (response.behavior === "deny") {
1953
+ this.emitToolItem(turn, call, "failed", detail, "Denied by user");
1954
+ if (response.interrupt) {
1955
+ turn.abort.abort();
1956
+ }
1957
+ const message = response.message?.trim();
1958
+ return message
1959
+ ? `The user declined this tool call: ${message}`
1960
+ : "The user declined this tool call.";
1961
+ }
1962
+ }
1963
+ this.emitToolItem(turn, call, "running", { type: "unknown", input: args, output: null }, null);
1964
+ try {
1965
+ const result = await catalog.executeTool(tool.name, args, { signal: turn.abort.signal });
1966
+ const output = ottoResultToText(result);
1967
+ if (turn.abort.signal.aborted) {
1968
+ this.emitToolItem(turn, call, "canceled", { type: "unknown", input: args, output }, null);
1969
+ return output;
1970
+ }
1971
+ this.emitToolItem(turn, call, result.isError ? "failed" : "completed", { type: "unknown", input: args, output }, result.isError ? output : null);
1972
+ return output;
1973
+ }
1974
+ catch (error) {
1975
+ const message = error instanceof Error ? error.message : String(error);
1976
+ this.emitToolItem(turn, call, "failed", { type: "unknown", input: args, output: null }, message);
1977
+ return `Error: ${message}`;
1978
+ }
1979
+ }
1980
+ emitToolItem(turn, call, status, detail, error) {
1981
+ this.emit({
1982
+ type: "timeline",
1983
+ provider: this.provider,
1984
+ turnId: turn.turnId,
1985
+ item: status === "failed"
1986
+ ? {
1987
+ type: "tool_call",
1988
+ callId: call.id,
1989
+ name: call.name,
1990
+ detail,
1991
+ status,
1992
+ error: error ?? "Tool failed",
1993
+ }
1994
+ : { type: "tool_call", callId: call.id, name: call.name, detail, status, error: null },
1995
+ });
1996
+ }
1997
+ async requestPermission(turn, input) {
1998
+ const request = {
1999
+ id: randomUUID(),
2000
+ provider: this.provider,
2001
+ name: input.name,
2002
+ kind: "tool",
2003
+ title: input.title,
2004
+ description: input.description,
2005
+ input: input.args,
2006
+ detail: input.detail,
2007
+ };
2008
+ const response = await new Promise((resolve) => {
2009
+ this.pendingPermissions.set(request.id, { request, resolve });
2010
+ this.emit({
2011
+ type: "permission_requested",
2012
+ provider: this.provider,
2013
+ request,
2014
+ turnId: turn.turnId,
2015
+ });
2016
+ });
2017
+ return response;
2018
+ }
2019
+ async streamCompletion(turn) {
2020
+ const endpoint = resolveEndpoint(this.env, this.label);
2021
+ const model = await this.resolveModel(endpoint);
2022
+ turn.assistantMessageId = randomUUID();
2023
+ turn.roundText = "";
2024
+ turn.roundReasoning = "";
2025
+ turn.pendingToolCalls = new Map();
2026
+ turn.finishReason = null;
2027
+ const toolSpecs = this.availableToolSpecs();
2028
+ const toolsPayload = [
2029
+ ...buildOpenAIToolsPayload(toolSpecs),
2030
+ ...this.buildOttoToolPayload(),
2031
+ ...this.buildMcpToolPayload(),
2032
+ ];
2033
+ const response = await fetch(`${endpoint.baseUrl}/chat/completions`, {
2034
+ method: "POST",
2035
+ headers: buildHeaders(endpoint),
2036
+ signal: turn.abort.signal,
2037
+ body: JSON.stringify({
2038
+ model,
2039
+ messages: this.messages.map(toWireMessage),
2040
+ stream: true,
2041
+ stream_options: { include_usage: true },
2042
+ ...(this.reasoningEffort !== "off" ? { reasoning_effort: this.reasoningEffort } : {}),
2043
+ ...(toolsPayload.length > 0 ? { tools: toolsPayload, tool_choice: "auto" } : {}),
2044
+ }),
2045
+ });
2046
+ if (!response.ok) {
2047
+ const bodyText = await response.text().catch(() => "");
2048
+ throw new Error(`${this.label} responded ${response.status} to /chat/completions${bodyText ? `: ${bodyText.slice(0, 400)}` : ""}`);
2049
+ }
2050
+ if (!response.body) {
2051
+ throw new Error(`${this.label} returned an empty response body`);
2052
+ }
2053
+ for await (const line of readLines(response.body)) {
2054
+ if (!line.startsWith("data:"))
2055
+ continue;
2056
+ const payload = line.slice(5).trim();
2057
+ if (!payload || payload === "[DONE]")
2058
+ continue;
2059
+ this.applyStreamPayload(turn, payload);
2060
+ }
2061
+ }
2062
+ applyStreamPayload(turn, payload) {
2063
+ let parsed;
2064
+ try {
2065
+ parsed = JSON.parse(payload);
2066
+ }
2067
+ catch {
2068
+ return;
2069
+ }
2070
+ const delta = parseStreamChunk(parsed);
2071
+ if (delta.reasoning) {
2072
+ turn.roundReasoning += delta.reasoning;
2073
+ this.emit({
2074
+ type: "timeline",
2075
+ provider: this.provider,
2076
+ turnId: turn.turnId,
2077
+ item: { type: "reasoning", text: delta.reasoning },
2078
+ });
2079
+ }
2080
+ if (delta.content) {
2081
+ turn.roundText += delta.content;
2082
+ this.emit({
2083
+ type: "timeline",
2084
+ provider: this.provider,
2085
+ turnId: turn.turnId,
2086
+ item: {
2087
+ type: "assistant_message",
2088
+ text: delta.content,
2089
+ messageId: turn.assistantMessageId,
2090
+ },
2091
+ });
2092
+ }
2093
+ for (const toolCallDelta of delta.toolCalls) {
2094
+ const existing = turn.pendingToolCalls.get(toolCallDelta.index) ?? {
2095
+ id: "",
2096
+ name: "",
2097
+ argumentsJson: "",
2098
+ };
2099
+ turn.pendingToolCalls.set(toolCallDelta.index, {
2100
+ id: toolCallDelta.id ?? (existing.id || `call_${toolCallDelta.index}`),
2101
+ name: existing.name || (toolCallDelta.name ?? ""),
2102
+ argumentsJson: existing.argumentsJson + (toolCallDelta.argumentsChunk ?? ""),
2103
+ });
2104
+ }
2105
+ if (delta.finishReason) {
2106
+ turn.finishReason = delta.finishReason;
2107
+ }
2108
+ if (delta.usage) {
2109
+ turn.usage = delta.usage;
2110
+ this.emitStreamUsageUpdated(turn);
2111
+ }
2112
+ }
2113
+ /**
2114
+ * Emit a usage_updated event so the agent manager updates agent.lastUsage
2115
+ * and pushes the context usage to the client immediately, rather than
2116
+ * waiting for turn_completed at the end of the entire tool loop.
2117
+ *
2118
+ * resolveContextWindowMaxTokens() is async (it may probe the endpoint) and
2119
+ * this runs on the hot streaming path, so only the already-cached value is
2120
+ * included. The event is always emitted regardless — the client and agent
2121
+ * manager handle partial usage snapshots correctly.
2122
+ */
2123
+ emitStreamUsageUpdated(turn) {
2124
+ if (!turn.usage || typeof turn.usage.inputTokens !== "number") {
2125
+ return;
2126
+ }
2127
+ const usage = {
2128
+ ...turn.usage,
2129
+ contextWindowUsedTokens: turn.usage.inputTokens + (turn.usage.outputTokens ?? 0),
2130
+ };
2131
+ if (this.contextWindowMaxTokens !== null) {
2132
+ usage.contextWindowMaxTokens = this.contextWindowMaxTokens;
2133
+ }
2134
+ this.emit({
2135
+ type: "usage_updated",
2136
+ provider: this.provider,
2137
+ usage,
2138
+ turnId: turn.turnId,
2139
+ });
2140
+ }
2141
+ /**
2142
+ * A turn can end (interrupt or failure) between an assistant message's
2143
+ * tool_calls and their tool results — e.g. the user interrupts during call
2144
+ * #1 of 3. Strict OpenAI-compatible servers reject the next request over
2145
+ * such a conversation with a 400, so append synthetic results for the
2146
+ * unanswered calls. Resume gets the equivalent repair from
2147
+ * sanitizeRestoredMessages; this covers the live conversation.
2148
+ */
2149
+ repairDanglingToolCalls() {
2150
+ // Only the trailing round can dangle: earlier rounds pushed all their
2151
+ // results before the loop moved on.
2152
+ for (let index = this.messages.length - 1; index >= 0; index -= 1) {
2153
+ const message = this.messages[index];
2154
+ if (message.role === "user") {
2155
+ return;
2156
+ }
2157
+ if (message.role !== "assistant" || !message.tool_calls) {
2158
+ continue;
2159
+ }
2160
+ const answered = new Set(this.messages
2161
+ .slice(index + 1)
2162
+ .flatMap((candidate) => (candidate.role === "tool" ? [candidate.tool_call_id] : [])));
2163
+ for (const call of message.tool_calls) {
2164
+ if (!answered.has(call.id)) {
2165
+ this.messages.push({
2166
+ role: "tool",
2167
+ content: "[Tool call was interrupted before it completed]",
2168
+ tool_call_id: call.id,
2169
+ });
2170
+ }
2171
+ }
2172
+ return;
2173
+ }
2174
+ }
2175
+ settleTurnFailure(turn, error) {
2176
+ if (this.activeTurn !== turn) {
2177
+ return;
2178
+ }
2179
+ this.activeTurn = null;
2180
+ this.failPendingPermissions();
2181
+ this.repairDanglingToolCalls();
2182
+ if (turn.abort.signal.aborted) {
2183
+ this.emit({
2184
+ type: "turn_canceled",
2185
+ provider: this.provider,
2186
+ reason: "Interrupted",
2187
+ turnId: turn.turnId,
2188
+ });
2189
+ if (turn.roundText || turn.roundReasoning) {
2190
+ if (turn.roundText) {
2191
+ turn.finalTextParts.push(turn.roundText);
2192
+ }
2193
+ this.messages.push({
2194
+ role: "assistant",
2195
+ content: turn.roundText,
2196
+ ...(turn.roundReasoning ? { reasoning: turn.roundReasoning } : {}),
2197
+ });
2198
+ }
2199
+ turn.resolve({
2200
+ sessionId: this.id,
2201
+ finalText: turn.finalTextParts.join("\n\n"),
2202
+ timeline: [],
2203
+ canceled: true,
2204
+ });
2205
+ return;
2206
+ }
2207
+ const message = error instanceof Error ? error.message : String(error);
2208
+ this.logger?.warn({ err: error, provider: this.provider }, "OpenAI-compatible turn failed");
2209
+ this.emit({
2210
+ type: "timeline",
2211
+ provider: this.provider,
2212
+ turnId: turn.turnId,
2213
+ item: { type: "error", message },
2214
+ });
2215
+ this.emit({
2216
+ type: "turn_failed",
2217
+ provider: this.provider,
2218
+ error: message,
2219
+ turnId: turn.turnId,
2220
+ });
2221
+ turn.reject(error instanceof Error ? error : new Error(message));
2222
+ }
2223
+ failPendingPermissions() {
2224
+ // Map iterators tolerate deleting the current entry mid-iteration.
2225
+ for (const [requestId, pending] of this.pendingPermissions) {
2226
+ this.pendingPermissions.delete(requestId);
2227
+ pending.resolve({ behavior: "deny", message: "Turn ended", interrupt: false });
2228
+ }
2229
+ }
2230
+ async resolveModel(endpoint) {
2231
+ if (this.modelId) {
2232
+ return this.modelId;
2233
+ }
2234
+ let response;
2235
+ try {
2236
+ response = await fetch(`${endpoint.baseUrl}/models`, {
2237
+ headers: buildHeaders(endpoint),
2238
+ signal: AbortSignal.timeout(DEFAULT_CATALOG_TIMEOUT_MS),
2239
+ });
2240
+ }
2241
+ catch (error) {
2242
+ throw unreachableError(this.label, endpoint, error);
2243
+ }
2244
+ const modelIds = response.ok ? parseModelList(await response.json()) : [];
2245
+ const first = modelIds[0];
2246
+ if (!first) {
2247
+ throw new Error(`${this.label} has no models loaded. Load a model in the server, then try again.`);
2248
+ }
2249
+ this.modelId = first;
2250
+ return first;
2251
+ }
2252
+ /**
2253
+ * Best-effort context window discovery, cached per model. Checks the
2254
+ * standard /v1/models listing for extended context-length fields first,
2255
+ * then LM Studio's native /api/v0/models listing (same host, /v1 stripped),
2256
+ * which reports the loaded instance's actual window. Servers that expose
2257
+ * neither leave the meter without a max — never an error.
2258
+ */
2259
+ async resolveContextWindowMaxTokens() {
2260
+ const model = this.modelId;
2261
+ if (!model) {
2262
+ return null;
2263
+ }
2264
+ if (this.contextWindowProbedModel === model) {
2265
+ return this.contextWindowMaxTokens;
2266
+ }
2267
+ let endpoint;
2268
+ try {
2269
+ endpoint = resolveEndpoint(this.env, this.label);
2270
+ }
2271
+ catch {
2272
+ return null;
2273
+ }
2274
+ const candidateUrls = [
2275
+ `${endpoint.baseUrl}/models`,
2276
+ `${endpoint.baseUrl.replace(/\/v1$/u, "")}/api/v0/models`,
2277
+ ];
2278
+ let resolved = null;
2279
+ for (const url of candidateUrls) {
2280
+ try {
2281
+ const response = await fetch(url, {
2282
+ headers: buildHeaders(endpoint),
2283
+ signal: AbortSignal.timeout(DEFAULT_CATALOG_TIMEOUT_MS),
2284
+ });
2285
+ if (!response.ok)
2286
+ continue;
2287
+ const match = parseModelContextLengths(await response.json()).get(model);
2288
+ if (typeof match === "number") {
2289
+ resolved = match;
2290
+ break;
2291
+ }
2292
+ }
2293
+ catch {
2294
+ // Discovery is optional; unreachable probe endpoints are expected.
2295
+ }
2296
+ }
2297
+ this.contextWindowMaxTokens = resolved;
2298
+ this.contextWindowProbedModel = model;
2299
+ return resolved;
2300
+ }
2301
+ /**
2302
+ * Promote the raw per-request token counts into AgentUsage. The final
2303
+ * round's prompt already contains the full conversation, so prompt +
2304
+ * completion tokens is the context content size as the server measured it.
2305
+ */
2306
+ async buildTurnUsage(turn) {
2307
+ if (!turn.usage) {
2308
+ return undefined;
2309
+ }
2310
+ const usage = { ...turn.usage };
2311
+ if (typeof turn.usage.inputTokens === "number") {
2312
+ const usedTokens = turn.usage.inputTokens + (turn.usage.outputTokens ?? 0);
2313
+ usage.contextWindowUsedTokens = usedTokens;
2314
+ this.lastContextTokens = usedTokens;
2315
+ }
2316
+ const maxTokens = await this.resolveContextWindowMaxTokens();
2317
+ if (maxTokens !== null) {
2318
+ usage.contextWindowMaxTokens = maxTokens;
2319
+ }
2320
+ return usage;
2321
+ }
2322
+ /**
2323
+ * Context makeup for the client's popup. The daemon owns this provider's
2324
+ * entire prompt (system message, tool schemas, conversation), so the split
2325
+ * is computed locally: char-based estimates per category, scaled so the
2326
+ * counted total matches the server-measured context size when one exists.
2327
+ */
2328
+ async getContextUsage() {
2329
+ const maxTokens = await this.resolveContextWindowMaxTokens();
2330
+ if (maxTokens === null) {
2331
+ return null;
2332
+ }
2333
+ const systemMessage = this.messages.find((message) => message.role === "system");
2334
+ const conversation = this.messages.filter((message) => message.role !== "system");
2335
+ const toolsPayload = [
2336
+ ...buildOpenAIToolsPayload(this.availableToolSpecs()),
2337
+ ...this.buildOttoToolPayload(),
2338
+ ...this.buildMcpToolPayload(),
2339
+ ];
2340
+ let categories = [
2341
+ {
2342
+ name: "Messages",
2343
+ tokens: conversation.reduce((sum, message) => sum + estimateTokens(JSON.stringify(message)), 0),
2344
+ },
2345
+ { name: "Tools", tokens: estimateTokens(JSON.stringify(toolsPayload)) },
2346
+ { name: "System prompt", tokens: estimateTokens(systemMessage?.content ?? "") },
2347
+ ];
2348
+ let totalTokens = categories.reduce((sum, category) => sum + category.tokens, 0);
2349
+ if (this.lastContextTokens !== null && totalTokens > 0) {
2350
+ const scale = this.lastContextTokens / totalTokens;
2351
+ categories = categories.map((category) => ({
2352
+ ...category,
2353
+ tokens: Math.round(category.tokens * scale),
2354
+ }));
2355
+ totalTokens = this.lastContextTokens;
2356
+ }
2357
+ return {
2358
+ categories: categories.filter((category) => category.tokens > 0),
2359
+ totalTokens,
2360
+ maxTokens,
2361
+ };
2362
+ }
2363
+ subscribe(callback) {
2364
+ this.listeners.add(callback);
2365
+ return () => {
2366
+ this.listeners.delete(callback);
2367
+ };
2368
+ }
2369
+ async *streamHistory() {
2370
+ for (const event of this.eventHistory) {
2371
+ yield event;
2372
+ }
2373
+ }
2374
+ async getRuntimeInfo() {
2375
+ return {
2376
+ provider: this.provider,
2377
+ sessionId: this.id,
2378
+ model: this.modelId,
2379
+ modeId: this.modeId,
2380
+ };
2381
+ }
2382
+ async getAvailableModes() {
2383
+ return OPENAI_COMPAT_MODES;
2384
+ }
2385
+ async getCurrentMode() {
2386
+ return this.modeId;
2387
+ }
2388
+ async setMode(modeId) {
2389
+ if (!VALID_MODE_IDS.has(modeId)) {
2390
+ throw new Error(`Unknown mode: ${modeId}`);
2391
+ }
2392
+ this.modeId = modeId;
2393
+ this.emit({
2394
+ type: "mode_changed",
2395
+ provider: this.provider,
2396
+ currentModeId: modeId,
2397
+ availableModes: OPENAI_COMPAT_MODES,
2398
+ });
2399
+ }
2400
+ getPendingPermissions() {
2401
+ return [...this.pendingPermissions.values()].map((pending) => pending.request);
2402
+ }
2403
+ async respondToPermission(requestId, response) {
2404
+ const pending = this.pendingPermissions.get(requestId);
2405
+ if (!pending) {
2406
+ return undefined;
2407
+ }
2408
+ this.pendingPermissions.delete(requestId);
2409
+ this.emit({
2410
+ type: "permission_resolved",
2411
+ provider: this.provider,
2412
+ requestId,
2413
+ resolution: response,
2414
+ ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
2415
+ });
2416
+ pending.resolve(response);
2417
+ return undefined;
2418
+ }
2419
+ describePersistence() {
2420
+ return {
2421
+ provider: this.provider,
2422
+ sessionId: this.id,
2423
+ metadata: {
2424
+ model: this.modelId,
2425
+ modeId: this.modeId,
2426
+ messages: this.messages,
2427
+ },
2428
+ };
2429
+ }
2430
+ async setModel(modelId) {
2431
+ this.modelId = modelId;
2432
+ }
2433
+ /**
2434
+ * Rewind the daemon-owned conversation to just before the given user
2435
+ * message. The full conversation persists (no message cap), so any
2436
+ * previous user message is a valid rewind target — the not-found error
2437
+ * below only fires for a truly unknown messageId.
2438
+ */
2439
+ async revertConversation(input) {
2440
+ if (this.activeTurn) {
2441
+ throw new Error(`${this.label} cannot rewind while a turn is running`);
2442
+ }
2443
+ const index = this.messages.findIndex((message) => message.role === "user" && message.messageId === input.messageId);
2444
+ if (index === -1) {
2445
+ throw new Error("Message not found in this session's conversation — it may predate rewind support or was trimmed from persisted history.");
2446
+ }
2447
+ const retained = sanitizeRestoredMessages(this.messages.slice(0, index));
2448
+ this.messages.length = 0;
2449
+ this.messages.push(...retained);
2450
+ this.rebuildEventHistory();
2451
+ this.lastContextTokens = null;
2452
+ }
2453
+ async interrupt() {
2454
+ const turn = this.activeTurn;
2455
+ if (!turn) {
2456
+ return;
2457
+ }
2458
+ turn.abort.abort();
2459
+ // A turn parked on a permission prompt has no in-flight fetch to abort —
2460
+ // deny the prompt so the loop unwinds and settles as canceled.
2461
+ this.failPendingPermissions();
2462
+ }
2463
+ async close() {
2464
+ await this.interrupt();
2465
+ await this.mcpManager?.close();
2466
+ this.listeners.clear();
2467
+ }
2468
+ /**
2469
+ * Retain what streamHistory replays: user messages, assistant messages
2470
+ * (coalesced per messageId so streamed deltas don't accumulate one event
2471
+ * each), reasoning (coalesced per contiguous block), and tool calls
2472
+ * (coalesced per callId so a call's running → completed/failed/canceled
2473
+ * transitions collapse to its latest status instead of replaying every
2474
+ * intermediate state). Usage and permission events aren't part of the
2475
+ * displayed transcript and are dropped. Copies are stored so coalescing
2476
+ * never mutates an event a listener already received.
2477
+ */
2478
+ retainForHistory(event) {
2479
+ if (event.type !== "timeline") {
2480
+ return;
2481
+ }
2482
+ const item = event.item;
2483
+ if (item.type === "user_message") {
2484
+ this.eventHistory.push({ ...event, item: { ...item } });
2485
+ return;
2486
+ }
2487
+ if (item.type === "tool_call") {
2488
+ const existingIdx = this.eventHistory.findIndex((candidate) => candidate.type === "timeline" &&
2489
+ candidate.item.type === "tool_call" &&
2490
+ candidate.item.callId === item.callId);
2491
+ const copy = { ...event, item: { ...item } };
2492
+ if (existingIdx >= 0) {
2493
+ this.eventHistory[existingIdx] = copy;
2494
+ }
2495
+ else {
2496
+ this.eventHistory.push(copy);
2497
+ }
2498
+ return;
2499
+ }
2500
+ if (item.type === "reasoning") {
2501
+ // Reasoning deltas stream contiguously within a round, so appending to
2502
+ // a trailing reasoning event coalesces one block per round; assistant
2503
+ // text or a tool call in between starts the next block naturally.
2504
+ const last = this.eventHistory[this.eventHistory.length - 1];
2505
+ if (last?.type === "timeline" && last.item.type === "reasoning") {
2506
+ last.item.text += item.text;
2507
+ return;
2508
+ }
2509
+ this.eventHistory.push({ ...event, item: { ...item } });
2510
+ return;
2511
+ }
2512
+ if (item.type !== "assistant_message") {
2513
+ return;
2514
+ }
2515
+ const last = this.eventHistory[this.eventHistory.length - 1];
2516
+ if (last?.type === "timeline" &&
2517
+ last.item.type === "assistant_message" &&
2518
+ last.item.messageId === item.messageId) {
2519
+ last.item.text += item.text;
2520
+ return;
2521
+ }
2522
+ this.eventHistory.push({ ...event, item: { ...item } });
2523
+ }
2524
+ emit(event) {
2525
+ this.retainForHistory(event);
2526
+ for (const listener of this.listeners) {
2527
+ try {
2528
+ listener(event);
2529
+ }
2530
+ catch (error) {
2531
+ this.logger?.warn({ err: error }, "OpenAI-compatible listener failed");
2532
+ }
2533
+ }
2534
+ }
2535
+ }
2536
+ //# sourceMappingURL=openai-compat-agent.js.map