luca 2.0.0 → 3.0.2

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 (532) hide show
  1. package/.github/workflows/release.yaml +170 -0
  2. package/AGENTS.md +99 -0
  3. package/CLAUDE.md +123 -0
  4. package/CNAME +1 -0
  5. package/README.md +275 -9
  6. package/RUNME.md +56 -0
  7. package/assistants/codingAssistant/ABOUT.md +5 -0
  8. package/assistants/codingAssistant/CORE.md +33 -0
  9. package/assistants/codingAssistant/hooks.ts +21 -0
  10. package/assistants/codingAssistant/tools.ts +12 -0
  11. package/assistants/inkbot/ABOUT.md +16 -0
  12. package/assistants/inkbot/CORE.md +330 -0
  13. package/assistants/inkbot/hooks.ts +6 -0
  14. package/assistants/inkbot/tools.ts +53 -0
  15. package/assistants/researcher/ABOUT.md +5 -0
  16. package/assistants/researcher/CORE.md +46 -0
  17. package/assistants/researcher/hooks.ts +16 -0
  18. package/assistants/researcher/tools.ts +237 -0
  19. package/bun.lock +2667 -0
  20. package/bunfig.toml +3 -0
  21. package/commands/audit-docs.ts +740 -0
  22. package/commands/build-bootstrap.ts +117 -0
  23. package/commands/build-python-bridge.ts +42 -0
  24. package/commands/build-scaffolds.ts +175 -0
  25. package/commands/bundle-consumer-project.ts +521 -0
  26. package/commands/generate-api-docs.ts +114 -0
  27. package/commands/inkbot.ts +874 -0
  28. package/commands/release.ts +80 -0
  29. package/commands/try-all-challenges.ts +543 -0
  30. package/commands/try-challenge.ts +100 -0
  31. package/dist/agi/container.server.d.ts +63 -0
  32. package/dist/agi/container.server.d.ts.map +1 -0
  33. package/dist/agi/endpoints/ask.d.ts +20 -0
  34. package/dist/agi/endpoints/ask.d.ts.map +1 -0
  35. package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
  36. package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
  37. package/dist/agi/endpoints/conversations.d.ts +18 -0
  38. package/dist/agi/endpoints/conversations.d.ts.map +1 -0
  39. package/dist/agi/endpoints/experts.d.ts +8 -0
  40. package/dist/agi/endpoints/experts.d.ts.map +1 -0
  41. package/dist/agi/feature.d.ts +9 -0
  42. package/dist/agi/feature.d.ts.map +1 -0
  43. package/dist/agi/features/assistant.d.ts +509 -0
  44. package/dist/agi/features/assistant.d.ts.map +1 -0
  45. package/dist/agi/features/assistants-manager.d.ts +236 -0
  46. package/dist/agi/features/assistants-manager.d.ts.map +1 -0
  47. package/dist/agi/features/autonomous-assistant.d.ts +281 -0
  48. package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
  49. package/dist/agi/features/browser-use.d.ts +479 -0
  50. package/dist/agi/features/browser-use.d.ts.map +1 -0
  51. package/dist/agi/features/claude-code.d.ts +824 -0
  52. package/dist/agi/features/claude-code.d.ts.map +1 -0
  53. package/dist/agi/features/conversation-history.d.ts +245 -0
  54. package/dist/agi/features/conversation-history.d.ts.map +1 -0
  55. package/dist/agi/features/conversation.d.ts +464 -0
  56. package/dist/agi/features/conversation.d.ts.map +1 -0
  57. package/dist/agi/features/docs-reader.d.ts +72 -0
  58. package/dist/agi/features/docs-reader.d.ts.map +1 -0
  59. package/dist/agi/features/file-tools.d.ts +110 -0
  60. package/dist/agi/features/file-tools.d.ts.map +1 -0
  61. package/dist/agi/features/luca-coder.d.ts +323 -0
  62. package/dist/agi/features/luca-coder.d.ts.map +1 -0
  63. package/dist/agi/features/openai-codex.d.ts +381 -0
  64. package/dist/agi/features/openai-codex.d.ts.map +1 -0
  65. package/dist/agi/features/openapi.d.ts +200 -0
  66. package/dist/agi/features/openapi.d.ts.map +1 -0
  67. package/dist/agi/features/skills-library.d.ts +167 -0
  68. package/dist/agi/features/skills-library.d.ts.map +1 -0
  69. package/dist/agi/index.d.ts +5 -0
  70. package/dist/agi/index.d.ts.map +1 -0
  71. package/dist/agi/lib/interceptor-chain.d.ts +44 -0
  72. package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
  73. package/dist/agi/lib/token-counter.d.ts +13 -0
  74. package/dist/agi/lib/token-counter.d.ts.map +1 -0
  75. package/dist/bootstrap/generated.d.ts +5 -0
  76. package/dist/bootstrap/generated.d.ts.map +1 -0
  77. package/dist/browser.d.ts +12 -0
  78. package/dist/browser.d.ts.map +1 -0
  79. package/dist/bus.d.ts +29 -0
  80. package/dist/bus.d.ts.map +1 -0
  81. package/dist/cli/build-info.d.ts +4 -0
  82. package/dist/cli/build-info.d.ts.map +1 -0
  83. package/dist/cli/cli.d.ts +3 -12
  84. package/dist/cli/cli.d.ts.map +1 -0
  85. package/dist/client.d.ts +60 -0
  86. package/dist/client.d.ts.map +1 -0
  87. package/dist/clients/civitai/index.d.ts +472 -0
  88. package/dist/clients/civitai/index.d.ts.map +1 -0
  89. package/dist/clients/client-template.d.ts +30 -0
  90. package/dist/clients/client-template.d.ts.map +1 -0
  91. package/dist/clients/comfyui/index.d.ts +281 -0
  92. package/dist/clients/comfyui/index.d.ts.map +1 -0
  93. package/dist/clients/elevenlabs/index.d.ts +197 -0
  94. package/dist/clients/elevenlabs/index.d.ts.map +1 -0
  95. package/dist/clients/graph.d.ts +64 -0
  96. package/dist/clients/graph.d.ts.map +1 -0
  97. package/dist/clients/openai/index.d.ts +247 -0
  98. package/dist/clients/openai/index.d.ts.map +1 -0
  99. package/dist/clients/rest.d.ts +92 -0
  100. package/dist/clients/rest.d.ts.map +1 -0
  101. package/dist/clients/supabase/index.d.ts +176 -0
  102. package/dist/clients/supabase/index.d.ts.map +1 -0
  103. package/dist/clients/websocket.d.ts +127 -0
  104. package/dist/clients/websocket.d.ts.map +1 -0
  105. package/dist/command.d.ts +163 -0
  106. package/dist/command.d.ts.map +1 -0
  107. package/dist/commands/bootstrap.d.ts +20 -0
  108. package/dist/commands/bootstrap.d.ts.map +1 -0
  109. package/dist/commands/chat.d.ts +37 -0
  110. package/dist/commands/chat.d.ts.map +1 -0
  111. package/dist/commands/code.d.ts +28 -0
  112. package/dist/commands/code.d.ts.map +1 -0
  113. package/dist/commands/console.d.ts +22 -0
  114. package/dist/commands/console.d.ts.map +1 -0
  115. package/dist/commands/describe.d.ts +50 -0
  116. package/dist/commands/describe.d.ts.map +1 -0
  117. package/dist/commands/eval.d.ts +23 -0
  118. package/dist/commands/eval.d.ts.map +1 -0
  119. package/dist/commands/help.d.ts +25 -0
  120. package/dist/commands/help.d.ts.map +1 -0
  121. package/dist/commands/index.d.ts +18 -0
  122. package/dist/commands/index.d.ts.map +1 -0
  123. package/dist/commands/introspect.d.ts +24 -0
  124. package/dist/commands/introspect.d.ts.map +1 -0
  125. package/dist/commands/mcp.d.ts +35 -0
  126. package/dist/commands/mcp.d.ts.map +1 -0
  127. package/dist/commands/prompt.d.ts +38 -0
  128. package/dist/commands/prompt.d.ts.map +1 -0
  129. package/dist/commands/run.d.ts +24 -0
  130. package/dist/commands/run.d.ts.map +1 -0
  131. package/dist/commands/sandbox-mcp.d.ts +34 -0
  132. package/dist/commands/sandbox-mcp.d.ts.map +1 -0
  133. package/dist/commands/save-api-docs.d.ts +21 -0
  134. package/dist/commands/save-api-docs.d.ts.map +1 -0
  135. package/dist/commands/scaffold.d.ts +24 -0
  136. package/dist/commands/scaffold.d.ts.map +1 -0
  137. package/dist/commands/select.d.ts +22 -0
  138. package/dist/commands/select.d.ts.map +1 -0
  139. package/dist/commands/serve.d.ts +29 -0
  140. package/dist/commands/serve.d.ts.map +1 -0
  141. package/dist/container-describer.d.ts +144 -0
  142. package/dist/container-describer.d.ts.map +1 -0
  143. package/dist/container.d.ts +451 -0
  144. package/dist/container.d.ts.map +1 -0
  145. package/dist/endpoint.d.ts +113 -0
  146. package/dist/endpoint.d.ts.map +1 -0
  147. package/dist/feature.d.ts +47 -0
  148. package/dist/feature.d.ts.map +1 -0
  149. package/dist/graft.d.ts +29 -0
  150. package/dist/graft.d.ts.map +1 -0
  151. package/dist/hash-object.d.ts +8 -0
  152. package/dist/hash-object.d.ts.map +1 -0
  153. package/dist/helper.d.ts +209 -0
  154. package/dist/helper.d.ts.map +1 -0
  155. package/dist/introspection/generated.node.d.ts +44623 -0
  156. package/dist/introspection/generated.node.d.ts.map +1 -0
  157. package/dist/introspection/generated.web.d.ts +1412 -0
  158. package/dist/introspection/generated.web.d.ts.map +1 -0
  159. package/dist/introspection/index.d.ts +156 -0
  160. package/dist/introspection/index.d.ts.map +1 -0
  161. package/dist/introspection/scan.d.ts +147 -0
  162. package/dist/introspection/scan.d.ts.map +1 -0
  163. package/dist/node/container.d.ts +256 -0
  164. package/dist/node/container.d.ts.map +1 -0
  165. package/dist/node/feature.d.ts +9 -0
  166. package/dist/node/feature.d.ts.map +1 -0
  167. package/dist/node/features/container-link.d.ts +213 -0
  168. package/dist/node/features/container-link.d.ts.map +1 -0
  169. package/dist/node/features/content-db.d.ts +354 -0
  170. package/dist/node/features/content-db.d.ts.map +1 -0
  171. package/dist/node/features/disk-cache.d.ts +236 -0
  172. package/dist/node/features/disk-cache.d.ts.map +1 -0
  173. package/dist/node/features/dns.d.ts +511 -0
  174. package/dist/node/features/dns.d.ts.map +1 -0
  175. package/dist/node/features/docker.d.ts +485 -0
  176. package/dist/node/features/docker.d.ts.map +1 -0
  177. package/dist/node/features/downloader.d.ts +73 -0
  178. package/dist/node/features/downloader.d.ts.map +1 -0
  179. package/dist/node/features/figlet-fonts.d.ts +4 -0
  180. package/dist/node/features/figlet-fonts.d.ts.map +1 -0
  181. package/dist/node/features/file-manager.d.ts +177 -0
  182. package/dist/node/features/file-manager.d.ts.map +1 -0
  183. package/dist/node/features/fs.d.ts +635 -0
  184. package/dist/node/features/fs.d.ts.map +1 -0
  185. package/dist/node/features/git.d.ts +329 -0
  186. package/dist/node/features/git.d.ts.map +1 -0
  187. package/dist/node/features/google-auth.d.ts +200 -0
  188. package/dist/node/features/google-auth.d.ts.map +1 -0
  189. package/dist/node/features/google-calendar.d.ts +194 -0
  190. package/dist/node/features/google-calendar.d.ts.map +1 -0
  191. package/dist/node/features/google-docs.d.ts +138 -0
  192. package/dist/node/features/google-docs.d.ts.map +1 -0
  193. package/dist/node/features/google-drive.d.ts +202 -0
  194. package/dist/node/features/google-drive.d.ts.map +1 -0
  195. package/dist/node/features/google-mail.d.ts +221 -0
  196. package/dist/node/features/google-mail.d.ts.map +1 -0
  197. package/dist/node/features/google-sheets.d.ts +157 -0
  198. package/dist/node/features/google-sheets.d.ts.map +1 -0
  199. package/dist/node/features/grep.d.ts +207 -0
  200. package/dist/node/features/grep.d.ts.map +1 -0
  201. package/dist/node/features/helpers.d.ts +236 -0
  202. package/dist/node/features/helpers.d.ts.map +1 -0
  203. package/dist/node/features/ink.d.ts +332 -0
  204. package/dist/node/features/ink.d.ts.map +1 -0
  205. package/dist/node/features/ipc-socket.d.ts +298 -0
  206. package/dist/node/features/ipc-socket.d.ts.map +1 -0
  207. package/dist/node/features/json-tree.d.ts +140 -0
  208. package/dist/node/features/json-tree.d.ts.map +1 -0
  209. package/dist/node/features/networking.d.ts +373 -0
  210. package/dist/node/features/networking.d.ts.map +1 -0
  211. package/dist/node/features/nlp.d.ts +125 -0
  212. package/dist/node/features/nlp.d.ts.map +1 -0
  213. package/dist/node/features/opener.d.ts +93 -0
  214. package/dist/node/features/opener.d.ts.map +1 -0
  215. package/dist/node/features/os.d.ts +168 -0
  216. package/dist/node/features/os.d.ts.map +1 -0
  217. package/dist/node/features/package-finder.d.ts +419 -0
  218. package/dist/node/features/package-finder.d.ts.map +1 -0
  219. package/dist/node/features/postgres.d.ts +173 -0
  220. package/dist/node/features/postgres.d.ts.map +1 -0
  221. package/dist/node/features/proc.d.ts +285 -0
  222. package/dist/node/features/proc.d.ts.map +1 -0
  223. package/dist/node/features/process-manager.d.ts +427 -0
  224. package/dist/node/features/process-manager.d.ts.map +1 -0
  225. package/dist/node/features/python.d.ts +477 -0
  226. package/dist/node/features/python.d.ts.map +1 -0
  227. package/dist/node/features/redis.d.ts +247 -0
  228. package/dist/node/features/redis.d.ts.map +1 -0
  229. package/dist/node/features/repl.d.ts +84 -0
  230. package/dist/node/features/repl.d.ts.map +1 -0
  231. package/dist/node/features/runpod.d.ts +527 -0
  232. package/dist/node/features/runpod.d.ts.map +1 -0
  233. package/dist/node/features/secure-shell.d.ts +145 -0
  234. package/dist/node/features/secure-shell.d.ts.map +1 -0
  235. package/dist/node/features/semantic-search.d.ts +207 -0
  236. package/dist/node/features/semantic-search.d.ts.map +1 -0
  237. package/dist/node/features/sqlite.d.ts +180 -0
  238. package/dist/node/features/sqlite.d.ts.map +1 -0
  239. package/dist/node/features/telegram.d.ts +173 -0
  240. package/dist/node/features/telegram.d.ts.map +1 -0
  241. package/dist/node/features/transpiler.d.ts +51 -0
  242. package/dist/node/features/transpiler.d.ts.map +1 -0
  243. package/dist/node/features/tts.d.ts +108 -0
  244. package/dist/node/features/tts.d.ts.map +1 -0
  245. package/dist/node/features/ui.d.ts +562 -0
  246. package/dist/node/features/ui.d.ts.map +1 -0
  247. package/dist/node/features/vault.d.ts +90 -0
  248. package/dist/node/features/vault.d.ts.map +1 -0
  249. package/dist/node/features/vm.d.ts +285 -0
  250. package/dist/node/features/vm.d.ts.map +1 -0
  251. package/dist/node/features/yaml-tree.d.ts +118 -0
  252. package/dist/node/features/yaml-tree.d.ts.map +1 -0
  253. package/dist/node/features/yaml.d.ts +127 -0
  254. package/dist/node/features/yaml.d.ts.map +1 -0
  255. package/dist/node.d.ts +67 -0
  256. package/dist/node.d.ts.map +1 -0
  257. package/dist/python/generated.d.ts +2 -0
  258. package/dist/python/generated.d.ts.map +1 -0
  259. package/dist/react/index.d.ts +36 -0
  260. package/dist/react/index.d.ts.map +1 -0
  261. package/dist/registry.d.ts +97 -0
  262. package/dist/registry.d.ts.map +1 -0
  263. package/dist/scaffolds/generated.d.ts +13 -0
  264. package/dist/scaffolds/generated.d.ts.map +1 -0
  265. package/dist/scaffolds/template.d.ts +11 -0
  266. package/dist/scaffolds/template.d.ts.map +1 -0
  267. package/dist/schemas/base.d.ts +254 -0
  268. package/dist/schemas/base.d.ts.map +1 -0
  269. package/dist/selector.d.ts +130 -0
  270. package/dist/selector.d.ts.map +1 -0
  271. package/dist/server.d.ts +89 -0
  272. package/dist/server.d.ts.map +1 -0
  273. package/dist/servers/express.d.ts +104 -0
  274. package/dist/servers/express.d.ts.map +1 -0
  275. package/dist/servers/mcp.d.ts +201 -0
  276. package/dist/servers/mcp.d.ts.map +1 -0
  277. package/dist/servers/socket.d.ts +121 -0
  278. package/dist/servers/socket.d.ts.map +1 -0
  279. package/dist/state.d.ts +24 -0
  280. package/dist/state.d.ts.map +1 -0
  281. package/dist/web/clients/socket.d.ts +37 -0
  282. package/dist/web/clients/socket.d.ts.map +1 -0
  283. package/dist/web/container.d.ts +55 -0
  284. package/dist/web/container.d.ts.map +1 -0
  285. package/dist/web/extension.d.ts +4 -0
  286. package/dist/web/extension.d.ts.map +1 -0
  287. package/dist/web/feature.d.ts +8 -0
  288. package/dist/web/feature.d.ts.map +1 -0
  289. package/dist/web/features/asset-loader.d.ts +35 -0
  290. package/dist/web/features/asset-loader.d.ts.map +1 -0
  291. package/dist/web/features/container-link.d.ts +167 -0
  292. package/dist/web/features/container-link.d.ts.map +1 -0
  293. package/dist/web/features/esbuild.d.ts +51 -0
  294. package/dist/web/features/esbuild.d.ts.map +1 -0
  295. package/dist/web/features/helpers.d.ts +140 -0
  296. package/dist/web/features/helpers.d.ts.map +1 -0
  297. package/dist/web/features/network.d.ts +69 -0
  298. package/dist/web/features/network.d.ts.map +1 -0
  299. package/dist/web/features/speech.d.ts +71 -0
  300. package/dist/web/features/speech.d.ts.map +1 -0
  301. package/dist/web/features/vault.d.ts +62 -0
  302. package/dist/web/features/vault.d.ts.map +1 -0
  303. package/dist/web/features/vm.d.ts +48 -0
  304. package/dist/web/features/vm.d.ts.map +1 -0
  305. package/dist/web/features/voice-recognition.d.ts +96 -0
  306. package/dist/web/features/voice-recognition.d.ts.map +1 -0
  307. package/dist/web/shims/isomorphic-vm.d.ts +22 -0
  308. package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
  309. package/index.html +1457 -0
  310. package/index.ts +1 -0
  311. package/install.sh +84 -0
  312. package/luca.cli.ts +16 -0
  313. package/luca.console.ts +9 -0
  314. package/main.py +6 -0
  315. package/package.json +219 -58
  316. package/public/index.html +1457 -0
  317. package/public/slides-ai-native.html +902 -0
  318. package/public/slides-intro.html +974 -0
  319. package/pyproject.toml +7 -0
  320. package/scripts/build-web.ts +28 -0
  321. package/scripts/examples/ask-luca-expert.ts +42 -0
  322. package/scripts/examples/assistant-questions.ts +12 -0
  323. package/scripts/examples/excalidraw-expert.ts +75 -0
  324. package/scripts/examples/expert-chat.ts +0 -0
  325. package/scripts/examples/file-manager.ts +14 -0
  326. package/scripts/examples/ideas.ts +12 -0
  327. package/scripts/examples/interactive-chat.ts +20 -0
  328. package/scripts/examples/openai-tool-calls.ts +113 -0
  329. package/scripts/examples/opening-a-web-browser.ts +5 -0
  330. package/scripts/examples/telegram-bot.ts +79 -0
  331. package/scripts/examples/using-assistant-with-mcp.ts +555 -0
  332. package/scripts/examples/using-claude-code.ts +10 -0
  333. package/scripts/examples/using-contentdb.ts +35 -0
  334. package/scripts/examples/using-conversations.ts +35 -0
  335. package/scripts/examples/using-disk-cache.ts +10 -0
  336. package/scripts/examples/using-docker-shell.ts +75 -0
  337. package/scripts/examples/using-elevenlabs.ts +25 -0
  338. package/scripts/examples/using-google-calendar.ts +57 -0
  339. package/scripts/examples/using-google-docs.ts +74 -0
  340. package/scripts/examples/using-google-drive.ts +74 -0
  341. package/scripts/examples/using-google-sheets.ts +89 -0
  342. package/scripts/examples/using-nlp.ts +55 -0
  343. package/scripts/examples/using-ollama.ts +11 -0
  344. package/scripts/examples/using-postgres.ts +55 -0
  345. package/scripts/examples/using-runpod.ts +32 -0
  346. package/scripts/examples/using-tts.ts +40 -0
  347. package/scripts/scaffold.ts +391 -0
  348. package/scripts/scratch.ts +15 -0
  349. package/scripts/stamp-build.sh +12 -0
  350. package/scripts/test-assistant-hooks.ts +13 -0
  351. package/scripts/test-docs-reader.ts +10 -0
  352. package/scripts/test-linux-binary.sh +80 -0
  353. package/scripts/update-introspection-data.ts +58 -0
  354. package/src/agi/README.md +14 -0
  355. package/src/agi/container.server.ts +156 -0
  356. package/src/agi/feature.ts +13 -0
  357. package/src/agi/features/agent-memory.ts +694 -0
  358. package/src/agi/features/assistant.ts +1653 -0
  359. package/src/agi/features/assistants-manager.ts +534 -0
  360. package/src/agi/features/autonomous-assistant.ts +431 -0
  361. package/src/agi/features/browser-use.ts +672 -0
  362. package/src/agi/features/claude-code.ts +1584 -0
  363. package/src/agi/features/coding-tools.ts +175 -0
  364. package/src/agi/features/conversation-history.ts +672 -0
  365. package/src/agi/features/conversation.ts +1494 -0
  366. package/src/agi/features/docs-reader.ts +167 -0
  367. package/src/agi/features/file-tools.ts +340 -0
  368. package/src/agi/features/luca-coder.ts +641 -0
  369. package/src/agi/features/mcp-bridge.ts +532 -0
  370. package/src/agi/features/openai-codex.ts +651 -0
  371. package/src/agi/features/openapi.ts +445 -0
  372. package/src/agi/features/skills-library.ts +557 -0
  373. package/src/agi/index.ts +6 -0
  374. package/src/agi/lib/interceptor-chain.ts +89 -0
  375. package/src/agi/lib/token-counter.ts +202 -0
  376. package/src/bootstrap/generated.ts +9791 -0
  377. package/src/browser.ts +25 -0
  378. package/src/bus.ts +122 -0
  379. package/src/cli/build-info.ts +4 -0
  380. package/src/cli/cli.ts +355 -0
  381. package/src/client.ts +170 -0
  382. package/src/clients/civitai/index.ts +537 -0
  383. package/src/clients/client-template.ts +41 -0
  384. package/src/clients/comfyui/index.ts +604 -0
  385. package/src/clients/elevenlabs/index.ts +317 -0
  386. package/src/clients/graph.ts +87 -0
  387. package/src/clients/openai/index.ts +456 -0
  388. package/src/clients/rest.ts +207 -0
  389. package/src/clients/supabase/index.ts +357 -0
  390. package/src/clients/voicebox/index.ts +300 -0
  391. package/src/clients/websocket.ts +251 -0
  392. package/src/command.ts +506 -0
  393. package/src/commands/bootstrap.ts +244 -0
  394. package/src/commands/chat.ts +309 -0
  395. package/src/commands/code.ts +371 -0
  396. package/src/commands/console.ts +189 -0
  397. package/src/commands/describe.ts +243 -0
  398. package/src/commands/eval.ts +67 -0
  399. package/src/commands/help.ts +240 -0
  400. package/src/commands/index.ts +19 -0
  401. package/src/commands/introspect.ts +218 -0
  402. package/src/commands/mcp.ts +64 -0
  403. package/src/commands/prompt.ts +1014 -0
  404. package/src/commands/run.ts +278 -0
  405. package/src/commands/sandbox-mcp.ts +343 -0
  406. package/src/commands/save-api-docs.ts +51 -0
  407. package/src/commands/scaffold.ts +225 -0
  408. package/src/commands/select.ts +99 -0
  409. package/src/commands/serve.ts +208 -0
  410. package/src/container-describer.ts +1091 -0
  411. package/src/container.ts +1199 -0
  412. package/src/endpoint.ts +365 -0
  413. package/src/entity.ts +173 -0
  414. package/src/feature.ts +118 -0
  415. package/src/graft.ts +181 -0
  416. package/src/hash-object.ts +97 -0
  417. package/src/helper.ts +849 -0
  418. package/src/introspection/generated.agi.ts +41200 -0
  419. package/src/introspection/generated.node.ts +28773 -0
  420. package/src/introspection/generated.web.ts +2272 -0
  421. package/src/introspection/index.ts +296 -0
  422. package/src/introspection/scan.ts +1136 -0
  423. package/src/node/container.ts +409 -0
  424. package/src/node/feature.ts +13 -0
  425. package/src/node/features/container-link.ts +559 -0
  426. package/src/node/features/content-db.ts +849 -0
  427. package/src/node/features/disk-cache.ts +388 -0
  428. package/src/node/features/display-result.ts +57 -0
  429. package/src/node/features/dns.ts +669 -0
  430. package/src/node/features/docker.ts +921 -0
  431. package/src/node/features/downloader.ts +79 -0
  432. package/src/node/features/figlet-fonts.ts +600 -0
  433. package/src/node/features/file-manager.ts +535 -0
  434. package/src/node/features/fs.ts +1050 -0
  435. package/src/node/features/git.ts +592 -0
  436. package/src/node/features/google-auth.ts +504 -0
  437. package/src/node/features/google-calendar.ts +306 -0
  438. package/src/node/features/google-docs.ts +412 -0
  439. package/src/node/features/google-drive.ts +346 -0
  440. package/src/node/features/google-mail.ts +540 -0
  441. package/src/node/features/google-sheets.ts +286 -0
  442. package/src/node/features/grep.ts +427 -0
  443. package/src/node/features/helpers.ts +762 -0
  444. package/src/node/features/ink.ts +490 -0
  445. package/src/node/features/ipc-socket.ts +649 -0
  446. package/src/node/features/json-tree.ts +170 -0
  447. package/src/node/features/networking.ts +961 -0
  448. package/src/node/features/nlp.ts +212 -0
  449. package/src/node/features/opener.ts +180 -0
  450. package/src/node/features/os.ts +403 -0
  451. package/src/node/features/package-finder.ts +540 -0
  452. package/src/node/features/postgres.ts +289 -0
  453. package/src/node/features/proc.ts +503 -0
  454. package/src/node/features/process-manager.ts +844 -0
  455. package/src/node/features/python.ts +912 -0
  456. package/src/node/features/redis.ts +446 -0
  457. package/src/node/features/repl.ts +212 -0
  458. package/src/node/features/runpod.ts +811 -0
  459. package/src/node/features/secure-shell.ts +261 -0
  460. package/src/node/features/semantic-search.ts +935 -0
  461. package/src/node/features/sqlite.ts +289 -0
  462. package/src/node/features/telegram.ts +343 -0
  463. package/src/node/features/transpiler.ts +160 -0
  464. package/src/node/features/tts.ts +185 -0
  465. package/src/node/features/ui.ts +791 -0
  466. package/src/node/features/vault.ts +153 -0
  467. package/src/node/features/vm.ts +462 -0
  468. package/src/node/features/yaml-tree.ts +148 -0
  469. package/src/node/features/yaml.ts +133 -0
  470. package/src/node.ts +76 -0
  471. package/src/python/bridge.py +220 -0
  472. package/src/python/generated.ts +226 -0
  473. package/src/react/index.ts +175 -0
  474. package/src/registry.ts +210 -0
  475. package/src/scaffolds/generated.ts +1814 -0
  476. package/src/scaffolds/template.ts +46 -0
  477. package/src/schemas/base.ts +296 -0
  478. package/src/selector.ts +352 -0
  479. package/src/server.ts +229 -0
  480. package/src/servers/express.ts +283 -0
  481. package/src/servers/mcp.ts +802 -0
  482. package/src/servers/socket.ts +258 -0
  483. package/src/state.ts +101 -0
  484. package/src/web/clients/socket.ts +99 -0
  485. package/src/web/container.ts +75 -0
  486. package/src/web/extension.ts +30 -0
  487. package/src/web/feature.ts +12 -0
  488. package/src/web/features/asset-loader.ts +72 -0
  489. package/src/web/features/container-link.ts +382 -0
  490. package/src/web/features/esbuild.ts +93 -0
  491. package/src/web/features/helpers.ts +291 -0
  492. package/src/web/features/network.ts +85 -0
  493. package/src/web/features/speech.ts +104 -0
  494. package/src/web/features/vault.ts +207 -0
  495. package/src/web/features/vm.ts +85 -0
  496. package/src/web/features/voice-recognition.ts +161 -0
  497. package/src/web/shims/isomorphic-vm.ts +149 -0
  498. package/tsconfig.build.json +12 -0
  499. package/tsconfig.json +58 -0
  500. package/uv.lock +8 -0
  501. package/LICENSE +0 -21
  502. package/dist/cli/cli.js +0 -48
  503. package/dist/cli/common.d.ts +0 -2
  504. package/dist/cli/common.js +0 -6
  505. package/dist/cli/index.d.ts +0 -2
  506. package/dist/cli/index.js +0 -5
  507. package/dist/cli/run.d.ts +0 -1
  508. package/dist/cli/run.js +0 -38
  509. package/dist/core/index.d.ts +0 -4
  510. package/dist/core/index.js +0 -32
  511. package/dist/core/read.d.ts +0 -2
  512. package/dist/core/read.js +0 -29
  513. package/dist/core/request.d.ts +0 -1
  514. package/dist/core/request.js +0 -2
  515. package/dist/core/write.d.ts +0 -2
  516. package/dist/core/write.js +0 -21
  517. package/dist/index.d.ts +0 -1
  518. package/dist/index.js +0 -5
  519. package/dist/utils/common.d.ts +0 -9
  520. package/dist/utils/common.js +0 -57
  521. package/dist/utils/consts.d.ts +0 -3
  522. package/dist/utils/consts.js +0 -11
  523. package/dist/utils/dict.d.ts +0 -1
  524. package/dist/utils/dict.js +0 -7
  525. package/dist/utils/index.d.ts +0 -5
  526. package/dist/utils/index.js +0 -21
  527. package/dist/utils/log.d.ts +0 -1
  528. package/dist/utils/log.js +0 -5
  529. package/dist/utils/types.d.ts +0 -1
  530. package/dist/utils/types.js +0 -2
  531. package/dist/utils/utils.test.d.ts +0 -1
  532. package/dist/utils/utils.test.js +0 -7
@@ -0,0 +1,1494 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { type AvailableFeatures } from 'luca/feature'
4
+ import { Feature } from '../feature.js'
5
+ import type { OpenAIClient } from '../../clients/openai';
6
+ import type OpenAI from 'openai';
7
+ import type { ConversationHistory } from './conversation-history';
8
+ import { countMessageTokens, getContextWindow, calculateCost } from '../lib/token-counter.js';
9
+
10
+ declare module 'luca/feature' {
11
+ interface AvailableFeatures {
12
+ conversation: typeof Conversation
13
+ }
14
+ }
15
+
16
+ export type Message = OpenAI.Chat.Completions.ChatCompletionMessageParam
17
+
18
+ export type ContentPart =
19
+ | { type: 'text'; text: string }
20
+ | { type: 'image_url'; image_url: { url: string; detail?: 'low' | 'high' | 'auto' } }
21
+ | { type: 'input_audio'; data: string; format: 'mp3' | 'wav' }
22
+ | { type: 'input_file'; file_data: string; filename: string }
23
+
24
+ export interface ConversationTool {
25
+ handler: (...args: any[]) => Promise<any>
26
+ description: string
27
+ parameters: Record<string, any>
28
+ }
29
+
30
+ export interface ConversationMCPServer {
31
+ url: string
32
+ headers?: Record<string, string>
33
+ allowedTools?: string[] | { tool_names?: string[] }
34
+ requireApproval?: 'always' | 'never' | {
35
+ always?: { tool_names?: string[] }
36
+ never?: { tool_names?: string[] }
37
+ }
38
+ }
39
+
40
+ const INPUT_TOKEN_SIZES: Record<string, number> = {
41
+ tiny: 8_000,
42
+ small: 16_000,
43
+ medium: 32_000,
44
+ large: 64_000,
45
+ xlarge: 256_000,
46
+ }
47
+
48
+ function resolveMaxInputTokens(value: number | string | undefined): number | undefined {
49
+ if (value == null) return undefined
50
+ if (typeof value === 'number') return value
51
+ return INPUT_TOKEN_SIZES[value]
52
+ }
53
+
54
+ export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
55
+ /** A unique identifier for the conversation */
56
+ id: z.string().optional().describe('A unique identifier for the conversation'),
57
+ /** A human-readable title for the conversation */
58
+ title: z.string().optional().describe('A human-readable title for the conversation'),
59
+ /** A unique identifier for threads, an arbitrary grouping mechanism */
60
+ thread: z.string().optional().describe('A unique identifier for threads, an arbitrary grouping mechanism'),
61
+ /** Any available OpenAI model */
62
+ model: z.string().optional().describe('Any available OpenAI model'),
63
+ /** Initial message history to seed the conversation */
64
+ history: z.array(z.any()).optional().describe('Initial message history to seed the conversation'),
65
+ /** Tools the model can call during conversation */
66
+ tools: z.record(z.string(), z.any()).optional().describe('Tools the model can call during conversation'),
67
+ /** Remote MCP servers to expose as tools when using the OpenAI Responses API */
68
+ mcpServers: z.record(z.string(), z.any()).optional().describe('Remote MCP servers keyed by server label'),
69
+ /** Which OpenAI API to use for completions */
70
+ api: z.enum(['auto', 'responses', 'chat']).optional().describe('Completion API mode. auto uses Responses unless local=true'),
71
+ /** Tags for categorizing and searching this conversation */
72
+ tags: z.array(z.string()).optional().describe('Tags for categorizing and searching this conversation'),
73
+ /** Arbitrary metadata to attach to this conversation */
74
+ metadata: z.record(z.string(), z.any()).optional().describe('Arbitrary metadata to attach to this conversation'),
75
+
76
+ clientOptions: z.record(z.string(), z.any()).optional().describe('Options for the OpenAI client'), // the type of options for OpenAI client
77
+
78
+ local: z.boolean().optional().describe('Whether to use the local ollama models instead of the remote OpenAI models'),
79
+
80
+ /** Maximum number of output tokens per completion */
81
+ maxTokens: z.number().optional().describe('Maximum number of output tokens per completion (default 512)'),
82
+
83
+ /** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
84
+ temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2). Higher = more random, lower = more deterministic'),
85
+ /** Nucleus sampling: only consider tokens with top_p cumulative probability (0-1). */
86
+ topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1). Lower = more focused'),
87
+ /** Top-K sampling: only consider the K most likely tokens. Not supported by OpenAI — used with local/Anthropic models. */
88
+ topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
89
+ /** Penalizes tokens based on how often they already appeared (-2 to 2). */
90
+ frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2). Positive = discourage repetition'),
91
+ /** Penalizes tokens based on whether they appeared at all (-2 to 2). */
92
+ presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2). Positive = encourage new topics'),
93
+ /** Stop sequences — model stops generating when it encounters any of these strings. */
94
+ stop: z.array(z.string()).optional().describe('Stop sequences — generation halts when any of these strings is produced'),
95
+
96
+ /** Enable automatic compaction when estimated input tokens approach the context limit */
97
+ autoCompact: z.boolean().optional().describe('Enable automatic compaction when input tokens approach the context limit'),
98
+ /** Fraction of contextWindow at which auto-compact triggers (0.0–1.0, default 0.8) */
99
+ compactThreshold: z.number().min(0).max(1).optional().describe('Fraction of context window at which auto-compact triggers (default 0.8)'),
100
+ /** Override the inferred context window size for this model */
101
+ contextWindow: z.number().optional().describe('Override the inferred context window size for this model'),
102
+ /** Number of recent messages to preserve after compaction (default 4) */
103
+ compactKeepRecent: z.number().optional().describe('Number of recent messages to preserve after compaction (default 4)'),
104
+
105
+ /** Maximum input tokens to send to the API. When set, older messages are trimmed to stay within this budget, keeping the system prompt and most recent messages. Useful for avoiding long-context pricing tiers. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k — max before long-context pricing). */
106
+ maxInputTokens: z.union([
107
+ z.number(),
108
+ z.enum(['tiny', 'small', 'medium', 'large', 'xlarge']),
109
+ ]).default('large').describe('Maximum input tokens. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k). Defaults to large (64k)'),
110
+ })
111
+
112
+ export const ConversationStateSchema = FeatureStateSchema.extend({
113
+ id: z.string().describe('Unique identifier for this conversation instance'),
114
+ thread: z.string().describe('Thread identifier for grouping conversations'),
115
+ model: z.string().describe('The OpenAI model being used'),
116
+ messages: z.array(z.any()).describe('Full message history of the conversation'),
117
+ streaming: z.boolean().describe('Whether a streaming response is currently in progress'),
118
+ lastResponse: z.string().describe('The last assistant response text'),
119
+ toolCalls: z.number().describe('Total number of tool calls made in this conversation'),
120
+ api: z.enum(['responses', 'chat']).describe('Which completion API is active for this conversation'),
121
+ lastResponseId: z.string().nullable().describe('Most recent OpenAI Responses API response ID for continuing conversation state'),
122
+ tokenUsage: z.object({
123
+ prompt: z.number().describe('Total prompt tokens consumed'),
124
+ completion: z.number().describe('Total completion tokens consumed'),
125
+ total: z.number().describe('Total tokens consumed'),
126
+ cachedTokens: z.number().describe('Input tokens served from cache (billed at reduced rate)'),
127
+ reasoningTokens: z.number().describe('Output tokens used for reasoning (o-series models)'),
128
+ }).describe('Cumulative token usage statistics including detail breakdowns from the API'),
129
+ cost: z.object({
130
+ inputCost: z.number().describe('Estimated cost in dollars for input tokens'),
131
+ outputCost: z.number().describe('Estimated cost in dollars for output tokens'),
132
+ totalCost: z.number().describe('Estimated total cost in dollars'),
133
+ }).describe('Running cost estimate based on cumulative token usage and model pricing'),
134
+ estimatedInputTokens: z.number().describe('Estimated input token count for the current messages array'),
135
+ compactionCount: z.number().describe('Number of times compact() has been called'),
136
+ contextWindow: z.number().describe('The context window size for the current model'),
137
+ tools: z.record(z.string(), z.any()).describe('Active tools map including any runtime overrides'),
138
+ callMaxTokens: z.number().nullable().describe('Per-call max tokens override, cleared after each ask()'),
139
+
140
+ /** Sampling parameters — state is the runtime source of truth, seeded from options at construction. */
141
+ temperature: z.number().nullable().describe('Sampling temperature (0-2). Null means use model default'),
142
+ topP: z.number().nullable().describe('Nucleus sampling cutoff (0-1). Null means use model default'),
143
+ topK: z.number().nullable().describe('Top-K sampling. Null means use model default'),
144
+ frequencyPenalty: z.number().nullable().describe('Frequency penalty (-2 to 2). Null means use model default'),
145
+ presencePenalty: z.number().nullable().describe('Presence penalty (-2 to 2). Null means use model default'),
146
+ stop: z.array(z.string()).nullable().describe('Stop sequences. Null means none'),
147
+ maxTokens: z.number().nullable().describe('Maximum output tokens per completion. Null means use model default'),
148
+ })
149
+
150
+ export class ConversationAbortError extends Error {
151
+ /** The partial text accumulated before the abort. */
152
+ readonly partial: string
153
+
154
+ constructor(partial: string) {
155
+ super('Conversation aborted')
156
+ this.name = 'ConversationAbortError'
157
+ this.partial = partial
158
+ }
159
+ }
160
+
161
+ export const ConversationEventsSchema = FeatureEventsSchema.extend({
162
+ userMessage: z.tuple([z.any().describe('The user message content (string or ContentPart[])')]).describe('Fired when a user message is added to the conversation'),
163
+ aborted: z.tuple([z.string().describe('Partial text accumulated before the abort')]).describe('Fired when the conversation is aborted mid-response'),
164
+ turnStart: z.tuple([z.object({ turn: z.number(), isFollowUp: z.boolean() })]).describe('Fired at the start of each completion turn'),
165
+ turnEnd: z.tuple([z.object({ turn: z.number(), hasToolCalls: z.boolean() })]).describe('Fired at the end of each completion turn'),
166
+ toolCallsStart: z.tuple([z.any().describe('Array of tool call objects from the model')]).describe('Fired when the model begins a batch of tool calls'),
167
+ toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Parsed arguments object')]).describe('Fired before invoking a single tool handler'),
168
+ toolResult: z.tuple([z.string().describe('Tool name'), z.string().describe('Serialized result')]).describe('Fired after a tool handler returns successfully'),
169
+ toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error object or message')]).describe('Fired when a tool handler throws or the tool is unknown'),
170
+ toolCallsEnd: z.tuple([]).describe('Fired after all tool calls in a turn have been executed'),
171
+ chunk: z.tuple([z.string().describe('Text delta from the stream')]).describe('Fired for each streaming text delta'),
172
+ preview: z.tuple([z.string().describe('Accumulated text so far')]).describe('Fired after each chunk with the full accumulated text'),
173
+ response: z.tuple([z.string().describe('Final accumulated response text')]).describe('Fired when the final text response is produced'),
174
+ responseCompleted: z.tuple([z.any().describe('The completed OpenAI Response object')]).describe('Fired when the Responses API stream completes'),
175
+ rawEvent: z.tuple([z.any().describe('Raw stream event from the API')]).describe('Fired for every raw event from the Responses API stream'),
176
+ mcpEvent: z.tuple([z.any().describe('MCP-related stream event')]).describe('Fired for MCP-related events from the Responses API'),
177
+ summarizeStart: z.tuple([]).describe('Fired before generating a conversation summary'),
178
+ summarizeEnd: z.tuple([z.string().describe('The generated summary text')]).describe('Fired after the summary is generated'),
179
+ compactStart: z.tuple([z.object({ messageCount: z.number(), keepRecent: z.number() })]).describe('Fired before compacting the conversation history'),
180
+ compactEnd: z.tuple([z.object({ summary: z.string(), removedCount: z.number(), estimatedTokens: z.number(), compactionCount: z.number() })]).describe('Fired after compaction completes'),
181
+ autoCompactTriggered: z.tuple([z.object({ estimated: z.number(), limit: z.number(), contextWindow: z.number() })]).describe('Fired when auto-compact kicks in because tokens exceeded the threshold'),
182
+ }).describe('Conversation events')
183
+
184
+ export type ConversationOptions = z.infer<typeof ConversationOptionsSchema>
185
+ export type ConversationState = z.infer<typeof ConversationStateSchema>
186
+
187
+ export type AskOptions = {
188
+ maxTokens?: number
189
+ /**
190
+ * When provided, enables OpenAI Structured Outputs. The model is constrained
191
+ * to return JSON matching this Zod schema. The return value of ask() will be
192
+ * the parsed object instead of a raw string.
193
+ */
194
+ schema?: z.ZodType
195
+ }
196
+
197
+ export type ForkOptions = Omit<Partial<ConversationOptions>, 'history'> & {
198
+ /**
199
+ * Controls how much message history carries over to the fork.
200
+ * - `'full'` (default) — deep copy all messages
201
+ * - `'none'` — system prompt only, no chat history
202
+ * - `number` — keep system prompt + last N user/assistant exchanges
203
+ */
204
+ history?: 'full' | 'none' | number
205
+ }
206
+
207
+ /**
208
+ * Recursively set `additionalProperties: false` on every object-type node
209
+ * in a JSON Schema tree. OpenAI strict mode requires this at every level.
210
+ * Also ensures every object has a `required` array listing all its property keys.
211
+ */
212
+ function strictifySchema(schema: Record<string, any>): Record<string, any> {
213
+ const clone = { ...schema }
214
+
215
+ if (clone.type === 'object' && clone.properties) {
216
+ clone.additionalProperties = false
217
+ clone.required = Object.keys(clone.properties)
218
+ const props: Record<string, any> = {}
219
+ for (const [key, val] of Object.entries(clone.properties)) {
220
+ props[key] = strictifySchema(val as Record<string, any>)
221
+ }
222
+ clone.properties = props
223
+ }
224
+
225
+ if (clone.items) {
226
+ clone.items = strictifySchema(clone.items)
227
+ }
228
+
229
+ // anyOf / oneOf / allOf
230
+ for (const combiner of ['anyOf', 'oneOf', 'allOf'] as const) {
231
+ if (Array.isArray(clone[combiner])) {
232
+ clone[combiner] = clone[combiner].map((s: Record<string, any>) => strictifySchema(s))
233
+ }
234
+ }
235
+
236
+ return clone
237
+ }
238
+
239
+ /**
240
+ * A self-contained conversation with OpenAI that supports streaming,
241
+ * tool calling, and message state management.
242
+ *
243
+ * @extends Feature
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const conversation = container.feature('conversation', {
248
+ * model: 'gpt-4.1',
249
+ * tools: myToolMap,
250
+ * history: [{ role: 'system', content: 'You are a helpful assistant.' }]
251
+ * })
252
+ * const reply = await conversation.ask('What is the meaning of life?')
253
+ * ```
254
+ */
255
+ export class Conversation extends Feature<ConversationState, ConversationOptions> {
256
+ static override stateSchema = ConversationStateSchema
257
+ static override optionsSchema = ConversationOptionsSchema
258
+ static override eventsSchema = ConversationEventsSchema
259
+ static override shortcut = 'features.conversation' as const
260
+
261
+ static { Feature.register(this, 'conversation') }
262
+
263
+ /**
264
+ * Pluggable tool executor. Called for each tool invocation with the tool
265
+ * name, parsed args, and the default handler. Return the serialized result string.
266
+ * The Assistant replaces this to wire in beforeToolCall/afterToolCall interceptors.
267
+ */
268
+ toolExecutor: ((name: string, args: Record<string, any>, handler: (...args: any[]) => Promise<any>) => Promise<string>) | null = null
269
+
270
+ /** The active structured output schema for the current ask() call, if any. */
271
+ private _activeSchema: z.ZodType | null = null
272
+
273
+ /** AbortController for the current ask() call, if any. */
274
+ private _abortController: AbortController | null = null
275
+
276
+ /** Registered stubs: matched against user input to short-circuit the API with a canned response. */
277
+ private _stubs: Array<{ matcher: string | RegExp; response: string | (() => string) }> = []
278
+
279
+ /** Resolved max tokens: per-call override > state-level. Undefined means no limit (model default). */
280
+ private get maxTokens(): number | undefined {
281
+ return (this.state.get('callMaxTokens') as number | null) ?? (this.state.get('maxTokens') as number | null) ?? undefined
282
+ }
283
+
284
+ /** @returns Default state seeded from options: id, thread, model, initial history, and zero token usage. */
285
+ override get initialState(): ConversationState {
286
+ return {
287
+ ...super.initialState,
288
+ id: this.options.id || this.uuid,
289
+ thread: this.options.thread || 'default',
290
+ model: this.options.model || 'gpt-5',
291
+ messages: this.options.history || [],
292
+ streaming: false,
293
+ lastResponse: '',
294
+ toolCalls: 0,
295
+ api: this.apiMode,
296
+ lastResponseId: null,
297
+ tokenUsage: { prompt: 0, completion: 0, total: 0, cachedTokens: 0, reasoningTokens: 0 },
298
+ cost: { inputCost: 0, outputCost: 0, totalCost: 0 },
299
+ estimatedInputTokens: 0,
300
+ compactionCount: 0,
301
+ contextWindow: this.options.contextWindow || getContextWindow(this.options.model || 'gpt-5'),
302
+ tools: (this.options.tools || {}) as Record<string, ConversationTool>,
303
+ callMaxTokens: null,
304
+ temperature: this.options.temperature ?? null,
305
+ topP: this.options.topP ?? null,
306
+ topK: this.options.topK ?? null,
307
+ frequencyPenalty: this.options.frequencyPenalty ?? null,
308
+ presencePenalty: this.options.presencePenalty ?? null,
309
+ stop: this.options.stop ?? null,
310
+ maxTokens: this.options.maxTokens ?? null,
311
+ }
312
+ }
313
+
314
+ /** Returns the registered tools available for the model to call. */
315
+ get tools() : Record<string, ConversationTool> {
316
+ return (this.state.get('tools') || {}) as Record<string, ConversationTool>
317
+ }
318
+
319
+ get availableTools() {
320
+ return Object.keys(this.tools)
321
+ }
322
+
323
+ /**
324
+ * Add or replace a single tool by name.
325
+ * Uses the same format as tools passed at construction time.
326
+ */
327
+ addTool(name: string, tool: ConversationTool): this {
328
+ this.state.set('tools', { ...this.tools, [name]: tool })
329
+ return this
330
+ }
331
+
332
+ /**
333
+ * Remove a tool by name.
334
+ */
335
+ removeTool(name: string): this {
336
+ const current = { ...this.tools }
337
+ delete current[name]
338
+ this.state.set('tools', current)
339
+ return this
340
+ }
341
+
342
+ /**
343
+ * Merge new tools into the conversation, replacing any with the same name.
344
+ * Accepts the same Record<string, ConversationTool> format used at construction time.
345
+ */
346
+ updateTools(tools: Record<string, ConversationTool>): this {
347
+ this.state.set('tools', { ...this.tools, ...tools })
348
+ return this
349
+ }
350
+
351
+ /**
352
+ * Register a hardcoded stub response that bypasses the API when the user's message matches.
353
+ * Streaming is still simulated — chunk/preview events fire word-by-word.
354
+ *
355
+ * @param matcher - Exact string match, substring, or RegExp tested against user input
356
+ * @param response - The text to stream back, or a zero-arg function that returns it
357
+ *
358
+ * @example
359
+ * conversation.stub('hello', 'Hi there!')
360
+ * conversation.stub(/weather/i, () => 'Sunny and 72°F.')
361
+ */
362
+ stub(matcher: string | RegExp, response: string | (() => string)): this {
363
+ this._stubs.push({ matcher, response })
364
+ return this
365
+ }
366
+
367
+ /** Returns configured remote MCP servers keyed by server label. */
368
+ get mcpServers(): Record<string, ConversationMCPServer> {
369
+ return (this.options.mcpServers || {}) as Record<string, ConversationMCPServer>
370
+ }
371
+
372
+ /** Returns the full message history of the conversation. */
373
+ get messages(): Message[] {
374
+ return this.state.get('messages') || []
375
+ }
376
+
377
+ /**
378
+ * Fork the conversation into a new independent instance.
379
+ * The fork inherits the same system prompt, tools, and message history,
380
+ * but has its own identity and state — changes in either direction do not affect the other.
381
+ *
382
+ * @param overrides - Option overrides for the forked conversation. Supports a `history` field
383
+ * that controls how much context carries over:
384
+ * - `'full'` (default) — deep copy all messages
385
+ * - `'none'` — system prompt only, no chat history
386
+ * - `number` — keep the system prompt plus the last N user/assistant exchanges
387
+ *
388
+ * When called with an array, creates multiple independent forks in one call.
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * // Full context fork
393
+ * const fork = conversation.fork()
394
+ *
395
+ * // System prompt only — cheapest
396
+ * const lean = conversation.fork({ history: 'none', model: 'gpt-4o-mini' })
397
+ *
398
+ * // Last 3 exchanges + system prompt
399
+ * const recent = conversation.fork({ history: 3 })
400
+ *
401
+ * // Multiple forks at once
402
+ * const [a, b, c] = conversation.fork([
403
+ * { history: 'none' },
404
+ * { history: 'none' },
405
+ * { history: 5 },
406
+ * ])
407
+ * ```
408
+ */
409
+ fork(overrides?: ForkOptions): Conversation
410
+ fork(overrides?: ForkOptions[]): Conversation[]
411
+ fork(overrides: ForkOptions | ForkOptions[] = {}): Conversation | Conversation[] {
412
+ if (Array.isArray(overrides)) {
413
+ return overrides.map(o => this.fork(o))
414
+ }
415
+
416
+ const { history: historyMode = 'full', ...convOverrides } = overrides
417
+ const allMessages = JSON.parse(JSON.stringify(this.messages)) as Message[]
418
+
419
+ let history: Message[]
420
+ if (historyMode === 'none') {
421
+ // System prompt only
422
+ const systemMsg = allMessages.find(m => m.role === 'system' || m.role === 'developer')
423
+ history = systemMsg ? [systemMsg] : []
424
+ } else if (historyMode === 'full') {
425
+ history = allMessages
426
+ } else {
427
+ // Keep last N exchanges (user + assistant pairs) plus system prompt
428
+ const systemMsg = allMessages.find(m => m.role === 'system' || m.role === 'developer')
429
+ const nonSystem = allMessages.filter(m => m.role !== 'system' && m.role !== 'developer')
430
+
431
+ // Walk backwards counting user messages as exchange boundaries.
432
+ // An exchange starts at a user message and includes everything after it
433
+ // until the next user message (assistant replies, tool calls, etc.).
434
+ let exchangeCount = 0
435
+ let cutoff = 0
436
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
437
+ if (nonSystem[i]!.role === 'user') {
438
+ exchangeCount++
439
+ if (exchangeCount > historyMode) break
440
+ cutoff = i
441
+ }
442
+ }
443
+
444
+ const kept = nonSystem.slice(cutoff)
445
+ history = systemMsg ? [systemMsg, ...kept] : kept
446
+ }
447
+
448
+ const forked = this.container.feature('conversation', {
449
+ ...this.options,
450
+ id: undefined,
451
+ history,
452
+ tools: { ...this.tools },
453
+ ...convOverrides,
454
+ })
455
+
456
+ // Copy stubs so forked conversations match the same patterns
457
+ ;(forked as any)._stubs = [...this._stubs]
458
+
459
+ return forked
460
+ }
461
+
462
+ /**
463
+ * Fan out N questions in parallel using forked conversations, return the results.
464
+ * Each fork is independent and ephemeral — no history is saved.
465
+ *
466
+ * @param questions - Array of questions (strings) or objects with question + per-fork overrides
467
+ * @param defaults - Default fork options applied to all forks (individual overrides take precedence)
468
+ * @returns Array of response strings, one per question
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * const results = await conversation.research([
473
+ * "What are the pros of approach A?",
474
+ * "What are the pros of approach B?",
475
+ * ], { history: 'none', model: 'gpt-4o-mini' })
476
+ *
477
+ * // Per-fork overrides
478
+ * const results = await conversation.research([
479
+ * "Quick factual question",
480
+ * { question: "Needs recent context", forkOptions: { history: 5 } },
481
+ * ], { history: 'none' })
482
+ * ```
483
+ */
484
+ async research(
485
+ questions: (string | { question: string; forkOptions?: ForkOptions })[],
486
+ defaults: ForkOptions = {}
487
+ ): Promise<string[]> {
488
+ const forkConfigs = questions.map(q => ({
489
+ ...defaults,
490
+ ...(typeof q === 'string' ? {} : q.forkOptions),
491
+ }))
492
+
493
+ const forks = this.fork(forkConfigs)
494
+
495
+ return Promise.all(
496
+ forks.map((fork, i) => {
497
+ const q = questions[i]!
498
+ const question = typeof q === 'string' ? q : q.question
499
+ return fork.ask(question)
500
+ })
501
+ )
502
+ }
503
+
504
+ /** Returns the OpenAI model name being used for completions. */
505
+ get model(): string {
506
+ return this.state.get('model')!
507
+ }
508
+
509
+ /** Returns the active completion API mode after resolving auto/local behavior. */
510
+ get apiMode(): 'responses' | 'chat' {
511
+ const mode = this.options.api || 'auto'
512
+ if (mode === 'chat' || mode === 'responses') return mode
513
+ return this.options.local ? 'chat' : 'responses'
514
+ }
515
+
516
+ /** Whether a streaming response is currently in progress. */
517
+ get isStreaming(): boolean {
518
+ return !!this.state.get('streaming')
519
+ }
520
+
521
+ /**
522
+ * Abort the current ask() call. Cancels the in-flight network request and
523
+ * any pending tool executions. The ask() promise will reject with a
524
+ * ConversationAbortError whose `partial` property contains any text
525
+ * accumulated before the abort.
526
+ */
527
+ abort(): void {
528
+ this._abortController?.abort()
529
+ }
530
+
531
+ /**
532
+ * Returns the correct parameter name for limiting output tokens.
533
+ * Local models (LM Studio, Ollama) and legacy OpenAI models use max_tokens.
534
+ * Newer OpenAI models (gpt-4o+, gpt-4.1, gpt-5, o1, o3, o4) require max_completion_tokens.
535
+ */
536
+ private get maxTokensParam(): 'max_tokens' | 'max_completion_tokens' {
537
+ if (this.options.local) return 'max_tokens'
538
+
539
+ const model = this.model
540
+ const needsCompletionTokens = [
541
+ 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4',
542
+ ]
543
+
544
+ if (needsCompletionTokens.some((prefix) => model.startsWith(prefix))) {
545
+ return 'max_completion_tokens'
546
+ }
547
+
548
+ return 'max_tokens'
549
+ }
550
+
551
+ /** The context window size for the current model (from options override or auto-detected). */
552
+ get contextWindow(): number {
553
+ return this.options.contextWindow || getContextWindow(this.model)
554
+ }
555
+
556
+ /** Whether the conversation is approaching the context limit. */
557
+ get isNearContextLimit(): boolean {
558
+ const threshold = this.options.compactThreshold ?? 0.8
559
+ return this.estimateTokens() >= this.contextWindow * threshold
560
+ }
561
+
562
+ /**
563
+ * Estimate the input token count for the current messages array
564
+ * using the js-tiktoken tokenizer. Updates state.
565
+ */
566
+ estimateTokens(): number {
567
+ const count = countMessageTokens(this.messages, this.model)
568
+ this.state.set('estimatedInputTokens', count)
569
+ return count
570
+ }
571
+
572
+ /**
573
+ * Generate a summary of the conversation so far using the LLM.
574
+ * Read-only — does not modify messages.
575
+ */
576
+ async summarize(): Promise<string> {
577
+ this.emit('summarizeStart')
578
+
579
+ const transcript = this.messages
580
+ .map(m => {
581
+ const role = m.role
582
+ const content = typeof m.content === 'string'
583
+ ? m.content
584
+ : Array.isArray(m.content)
585
+ ? (m.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join('\n')
586
+ : (m.content != null ? JSON.stringify(m.content) : '(no content)')
587
+ return `[${role}]: ${content || '(no text content)'}`
588
+ })
589
+ .join('\n\n')
590
+
591
+ const response = await this.openai.raw.chat.completions.create({
592
+ model: this.model,
593
+ messages: [
594
+ {
595
+ role: 'system',
596
+ content: 'You are a conversation summarizer. Produce a concise but comprehensive summary of the following conversation. Preserve all key facts, decisions, context, user preferences, and any important details needed to continue the conversation. Output only the summary.',
597
+ },
598
+ { role: 'user', content: transcript },
599
+ ],
600
+ stream: false,
601
+ })
602
+
603
+ const summary = (response as any).choices?.[0]?.message?.content || ''
604
+ this.emit('summarizeEnd', summary)
605
+ return summary
606
+ }
607
+
608
+ /**
609
+ * Compact the conversation by summarizing old messages and replacing them
610
+ * with a summary message. Keeps the system message (if any) and the most
611
+ * recent N messages.
612
+ */
613
+ async compact(options?: { keepRecent?: number }): Promise<{ summary: string; removedCount: number; estimatedTokens: number }> {
614
+ const keepRecent = options?.keepRecent ?? this.options.compactKeepRecent ?? 4
615
+ const messages = this.messages
616
+
617
+ if (messages.length <= keepRecent + 1) {
618
+ return { summary: '', removedCount: 0, estimatedTokens: this.estimateTokens() }
619
+ }
620
+
621
+ this.emit('compactStart', { messageCount: messages.length, keepRecent })
622
+
623
+ const summary = await this.summarize()
624
+
625
+ const systemMessage = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
626
+ ? messages[0]
627
+ : null
628
+
629
+ let sliceStart = messages.length - keepRecent
630
+ // Walk back to avoid splitting a tool call group — if we'd start on a tool message,
631
+ // include the preceding assistant message (and its full tool response block)
632
+ if (sliceStart > 0) {
633
+ while (sliceStart > 0 && messages[sliceStart]?.role === 'tool') {
634
+ sliceStart--
635
+ }
636
+ }
637
+ const recentMessages = messages.slice(sliceStart)
638
+
639
+ const newMessages: Message[] = []
640
+ if (systemMessage) newMessages.push(systemMessage)
641
+
642
+ newMessages.push({
643
+ role: 'developer',
644
+ content: `[Conversation Summary — the following is a summary of the earlier conversation that has been compacted to save context space]\n\n${summary}`,
645
+ } as Message)
646
+
647
+ newMessages.push(...recentMessages)
648
+
649
+ const removedCount = messages.length - newMessages.length
650
+ this.state.set('messages', newMessages)
651
+ this.state.set('compactionCount', (this.state.get('compactionCount') || 0) + 1)
652
+
653
+ // Responses API: clear continuation chain since message history changed
654
+ if (this.apiMode === 'responses') {
655
+ this.state.set('lastResponseId', null)
656
+ }
657
+
658
+ const estimatedTokens = this.estimateTokens()
659
+
660
+ this.emit('compactEnd', { summary, removedCount, estimatedTokens, compactionCount: this.state.get('compactionCount') })
661
+
662
+ return { summary, removedCount, estimatedTokens }
663
+ }
664
+
665
+ /**
666
+ * Get the OpenAI-formatted tools array from the registered tools.
667
+ *
668
+ * @returns {OpenAI.Chat.Completions.ChatCompletionTool[]} The tools formatted for OpenAI
669
+ */
670
+ private get openaiTools(): OpenAI.Chat.Completions.ChatCompletionTool[] {
671
+ return Object.entries(this.tools).map(([name, tool]) => ({
672
+ type: 'function' as const,
673
+ function: {
674
+ name,
675
+ description: tool.description,
676
+ parameters: tool.parameters
677
+ }
678
+ }))
679
+ }
680
+
681
+ /**
682
+ * Get the OpenAI Responses-formatted tools array from local function tools
683
+ * plus configured remote MCP servers.
684
+ */
685
+ private get responseTools(): OpenAI.Responses.Tool[] {
686
+ const functionTools = Object.entries(this.tools).map(([name, tool]) => ({
687
+ type: 'function' as const,
688
+ name,
689
+ description: tool.description,
690
+ parameters: { ...tool.parameters, additionalProperties: false },
691
+ strict: true,
692
+ }))
693
+
694
+ const mcpTools = Object.entries(this.mcpServers)
695
+ .filter(([, server]) => !!server?.url)
696
+ .map(([serverLabel, server]) => ({
697
+ type: 'mcp' as const,
698
+ server_label: serverLabel,
699
+ server_url: server.url,
700
+ ...(server.headers ? { headers: server.headers } : {}),
701
+ ...(server.allowedTools ? { allowed_tools: server.allowedTools } : {}),
702
+ ...(server.requireApproval ? { require_approval: server.requireApproval } : {}),
703
+ }))
704
+
705
+ return [...functionTools, ...mcpTools]
706
+ }
707
+
708
+ /** Returns the first system/developer text message to use as Responses instructions. */
709
+ private get responsesInstructions(): string | undefined {
710
+ for (const message of this.messages) {
711
+ if ((message.role === 'system' || message.role === 'developer') && typeof message.content === 'string') {
712
+ return message.content
713
+ }
714
+ }
715
+ return undefined
716
+ }
717
+
718
+ /**
719
+ * Send a message and get a streamed response. Automatically handles
720
+ * tool calls by invoking the registered handlers and feeding results
721
+ * back to the model until a final text response is produced.
722
+ *
723
+ * @param {string | ContentPart[]} content - The user message, either a string or array of content parts (text + images)
724
+ * @returns {Promise<string>} The assistant's final text response
725
+ *
726
+ * @example
727
+ * const reply = await conversation.ask("What's the weather in SF?")
728
+ * // With image:
729
+ * const reply = await conversation.ask([
730
+ * { type: 'text', text: 'What is in this diagram?' },
731
+ * { type: 'image_url', image_url: { url: 'data:image/png;base64,...' } }
732
+ * ])
733
+ */
734
+ async ask(content: string | ContentPart[], options?: AskOptions): Promise<string> {
735
+ this.state.set('callMaxTokens', options?.maxTokens ?? null)
736
+ this._activeSchema = options?.schema ?? null
737
+ this._abortController = new AbortController()
738
+
739
+ // Auto-compact before adding the new message
740
+ if (this.options.autoCompact) {
741
+ const threshold = this.options.compactThreshold ?? 0.8
742
+ const estimated = this.estimateTokens()
743
+ const limit = this.contextWindow * threshold
744
+ if (estimated >= limit) {
745
+ this.emit('autoCompactTriggered', { estimated, limit, contextWindow: this.contextWindow })
746
+ await this.compact()
747
+ }
748
+ }
749
+
750
+ const userMessage: Message = { role: 'user', content: content as any }
751
+ this.pushMessage(userMessage)
752
+ this.emit('userMessage', content)
753
+
754
+ try {
755
+ const stubText = this._matchStub(typeof content === 'string' ? content : '')
756
+ if (stubText !== null) {
757
+ return await this._streamStub(stubText)
758
+ }
759
+
760
+ let raw: string
761
+
762
+ if (this.apiMode === 'responses') {
763
+ // When maxInputTokens is set, skip previous_response_id continuation
764
+ // so we control exactly how many tokens the API processes (server-side
765
+ // context from previous_response_id would accumulate unbounded).
766
+ const canChain = !this.options.maxInputTokens
767
+ const previousResponseId = canChain ? (this.state.get('lastResponseId') || undefined) : undefined
768
+ let input: OpenAI.Responses.ResponseInput
769
+
770
+ if (previousResponseId) {
771
+ // Can chain via previous_response_id — only send the new user message
772
+ input = [this.toResponsesUserMessage(content)]
773
+ } else {
774
+ // No previous response ID (first call, resumed from disk, or maxInputTokens active).
775
+ // Convert (possibly trimmed) message history to Responses API input.
776
+ input = this.messagesToResponsesInput(this.getMessagesWithinBudget())
777
+ }
778
+
779
+ raw = await this.runResponsesLoop({
780
+ turn: 1,
781
+ accumulated: '',
782
+ input,
783
+ previousResponseId,
784
+ })
785
+ } else {
786
+ raw = await this.runChatCompletionLoop({ turn: 1, accumulated: '' })
787
+ }
788
+
789
+ // When a structured output schema is active, parse the JSON response
790
+ if (this._activeSchema) {
791
+ try {
792
+ const parsed = JSON.parse(raw)
793
+ return parsed
794
+ } catch {
795
+ // Model returned something that isn't valid JSON — return raw
796
+ return raw
797
+ }
798
+ }
799
+
800
+ return raw
801
+ } catch (err: any) {
802
+ if (err instanceof ConversationAbortError) {
803
+ this.emit('aborted', err.partial)
804
+ throw err
805
+ }
806
+ // Re-throw abort errors from the OpenAI SDK / DOM AbortController
807
+ if (err.name === 'AbortError' || this._abortController?.signal.aborted) {
808
+ const partial = this.state.get('lastResponse') || ''
809
+ this.emit('aborted', partial)
810
+ throw new ConversationAbortError(partial)
811
+ }
812
+ throw err
813
+ } finally {
814
+ this.state.set('callMaxTokens', null)
815
+ this._activeSchema = null
816
+ this._abortController = null
817
+ }
818
+ }
819
+
820
+ /** Convert user content into a Responses API input message item. */
821
+ private toResponsesUserMessage(content: string | ContentPart[]): OpenAI.Responses.ResponseInputItem.Message {
822
+ if (typeof content === 'string') {
823
+ return {
824
+ type: 'message',
825
+ role: 'user',
826
+ content: [{ type: 'input_text', text: content }]
827
+ }
828
+ }
829
+
830
+ const parts = content.map((part) => {
831
+ if (part.type === 'text') {
832
+ return { type: 'input_text' as const, text: part.text }
833
+ }
834
+ if (part.type === 'input_audio') {
835
+ return { type: 'input_audio' as const, data: part.data, format: part.format }
836
+ }
837
+ if (part.type === 'input_file') {
838
+ return { type: 'input_file' as const, file_data: part.file_data, filename: part.filename }
839
+ }
840
+
841
+ return {
842
+ type: 'input_image' as const,
843
+ image_url: part.image_url.url,
844
+ detail: part.image_url.detail || 'auto',
845
+ }
846
+ }) as OpenAI.Responses.ResponseInputMessageContentList
847
+
848
+ return {
849
+ type: 'message',
850
+ role: 'user',
851
+ content: parts,
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Convert the full Chat Completions message history into Responses API input items.
857
+ * Used when resuming a conversation without a previous_response_id.
858
+ */
859
+ private messagesToResponsesInput(messages?: Message[]): OpenAI.Responses.ResponseInput {
860
+ const input: OpenAI.Responses.ResponseInput = []
861
+
862
+ for (const msg of (messages || this.messages)) {
863
+ if (msg.role === 'system' || msg.role === 'developer') {
864
+ // System/developer messages are handled via the instructions parameter
865
+ continue
866
+ }
867
+
868
+ if (msg.role === 'user') {
869
+ if (typeof msg.content === 'string') {
870
+ input.push({
871
+ type: 'message',
872
+ role: 'user',
873
+ content: [{ type: 'input_text', text: msg.content }],
874
+ })
875
+ } else if (Array.isArray(msg.content)) {
876
+ input.push(this.toResponsesUserMessage(msg.content as ContentPart[]))
877
+ }
878
+ continue
879
+ }
880
+
881
+ if (msg.role === 'assistant') {
882
+ const content = typeof msg.content === 'string' ? msg.content : (msg.content || []).map((p: any) => p.text || '').join('')
883
+ if (content) {
884
+ input.push({
885
+ type: 'message',
886
+ role: 'assistant',
887
+ content: [{ type: 'output_text', text: content, annotations: [] }],
888
+ id: `msg_replay-${input.length}`,
889
+ status: 'completed',
890
+ } as any)
891
+ }
892
+ continue
893
+ }
894
+
895
+ // Tool results — skip in the replay since the assistant's tool_calls won't have matching IDs
896
+ // The model will still understand context from the assistant messages that followed
897
+ }
898
+
899
+ return input
900
+ }
901
+
902
+ /**
903
+ * Build the OpenAI response_format / text.format config from the active Zod schema.
904
+ * Returns undefined when no schema is active.
905
+ */
906
+ private get structuredOutputConfig(): { name: string; schema: Record<string, any>; strict: true } | undefined {
907
+ if (!this._activeSchema) return undefined
908
+
909
+ const raw = (this._activeSchema as any).toJSONSchema() as Record<string, any>
910
+ const strict = strictifySchema(raw)
911
+
912
+ // Derive a name from the schema description or fall back to a default.
913
+ // OpenAI requires [a-zA-Z0-9_-] max 64 chars.
914
+ const desc = raw.description || 'structured_output'
915
+ const name = desc.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
916
+
917
+ return {
918
+ name,
919
+ schema: { type: strict.type || 'object', properties: strict.properties, required: strict.required, additionalProperties: false },
920
+ strict: true,
921
+ }
922
+ }
923
+
924
+ /** Returns the OpenAI client instance from the container. */
925
+ get openai() {
926
+ let baseURL = this.options.clientOptions?.baseURL ? this.options.clientOptions.baseURL : undefined
927
+
928
+ if (this.options.local) {
929
+ baseURL = "http://localhost:1234/v1"
930
+ }
931
+
932
+ return (this.container as any).client('openai', {
933
+ defaultModel: this.model || (this.options.local ? this.model || "qwen/qwen3-coder-30b" : "gpt-5"),
934
+ ...this.options.clientOptions,
935
+ ...(baseURL ? { baseURL } : {}),
936
+ }) as OpenAIClient
937
+ }
938
+
939
+ /** Returns the conversationHistory feature for persistence. */
940
+ get history(): ConversationHistory {
941
+ return this.container.feature('conversationHistory') as ConversationHistory
942
+ }
943
+
944
+ /**
945
+ * Persist this conversation to disk via conversationHistory.
946
+ * Creates a new record if this conversation hasn't been saved before,
947
+ * or updates the existing one.
948
+ *
949
+ * @param opts - Optional overrides for title, tags, thread, or metadata
950
+ * @returns The saved conversation record
951
+ */
952
+ async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
953
+ const id = this.state.get('id')!
954
+ const existing = await this.history.load(id)
955
+
956
+ // Persist lastResponseId so the Responses API can continue the chain on resume
957
+ const lastResponseId = this.state.get('lastResponseId')
958
+ const responseMeta = lastResponseId ? { lastResponseId } : {}
959
+
960
+ // Grab the live token usage and cost from state
961
+ const tokenUsage = this.state.get('tokenUsage')!
962
+ const cost = this.state.get('cost')!
963
+
964
+ if (existing) {
965
+ existing.messages = this.messages
966
+ existing.model = this.model
967
+ existing.tokenUsage = tokenUsage
968
+ existing.cost = cost
969
+ if (opts?.title) existing.title = opts.title
970
+ if (opts?.tags) existing.tags = opts.tags
971
+ if (opts?.thread) existing.thread = opts.thread
972
+ existing.metadata = { ...existing.metadata, ...responseMeta, ...(opts?.metadata || {}) }
973
+ await this.history.save(existing)
974
+ return existing
975
+ }
976
+
977
+ return this.history.create({
978
+ id,
979
+ title: opts?.title || this.options.title,
980
+ model: this.model,
981
+ messages: this.messages,
982
+ tags: opts?.tags || this.options.tags || [],
983
+ thread: opts?.thread || this.options.thread || this.state.get('thread'),
984
+ tokenUsage,
985
+ cost,
986
+ metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
987
+ })
988
+ }
989
+
990
+ /**
991
+ * Execute a single tool call, routing through the pluggable toolExecutor
992
+ * if one is set (e.g. by the Assistant's interceptor chain).
993
+ */
994
+ private async executeTool(toolName: string, rawArgs: string): Promise<string> {
995
+ const tool = this.tools[toolName]
996
+ const callCount = (this.state.get('toolCalls') || 0) + 1
997
+ this.state.set('toolCalls', callCount)
998
+
999
+ if (!tool) {
1000
+ const result = JSON.stringify({ error: `Unknown tool: ${toolName}` })
1001
+ this.emit('toolError', toolName, result)
1002
+ return result
1003
+ }
1004
+
1005
+ let args: Record<string, any>
1006
+ try {
1007
+ args = rawArgs ? JSON.parse(rawArgs) : {}
1008
+ } catch (parseErr: any) {
1009
+ const result = JSON.stringify({ error: `Failed to parse tool arguments: ${parseErr.message}`, rawArgs })
1010
+ this.emit('toolError', toolName, parseErr)
1011
+ return result
1012
+ }
1013
+
1014
+ if (this.toolExecutor) {
1015
+ return this.toolExecutor(toolName, args, tool.handler)
1016
+ }
1017
+
1018
+ try {
1019
+ this.emit('toolCall', toolName, args)
1020
+ const output = await tool.handler(args)
1021
+ const result = typeof output === 'string' ? output : JSON.stringify(output)
1022
+ this.emit('toolResult', toolName, result)
1023
+ return result
1024
+ } catch (err: any) {
1025
+ const result = JSON.stringify({ error: err.message || String(err) })
1026
+ this.emit('toolError', toolName, err)
1027
+ return result
1028
+ }
1029
+ }
1030
+
1031
+ /** Check registered stubs against user input. Returns the response text, or null if no match. */
1032
+ private _matchStub(input: string): string | null {
1033
+ for (const { matcher, response } of this._stubs) {
1034
+ const matched = typeof matcher === 'string'
1035
+ ? input === matcher || input.includes(matcher)
1036
+ : matcher.test(input)
1037
+ if (matched) {
1038
+ return typeof response === 'function' ? response() : response
1039
+ }
1040
+ }
1041
+ return null
1042
+ }
1043
+
1044
+ /**
1045
+ * Simulate a streaming response for a hardcoded stub text.
1046
+ * Emits chunk/preview events word-by-word, yielding between each to keep the event loop alive.
1047
+ */
1048
+ private async _streamStub(text: string): Promise<string> {
1049
+ this.state.set('streaming', true)
1050
+ this.emit('turnStart', { turn: 1, isFollowUp: false })
1051
+
1052
+ let accumulated = ''
1053
+ const chunks = text.match(/\S+\s*/g) ?? [text]
1054
+
1055
+ try {
1056
+ for (const chunk of chunks) {
1057
+ accumulated += chunk
1058
+ this.emit('chunk', chunk)
1059
+ this.emit('preview', accumulated)
1060
+ await Promise.resolve()
1061
+ }
1062
+ } finally {
1063
+ this.state.set('streaming', false)
1064
+ }
1065
+
1066
+ const trimmed = text
1067
+ this.pushMessage({ role: 'assistant', content: trimmed })
1068
+ this.state.set('lastResponse', trimmed)
1069
+ this.emit('turnEnd', { turn: 1, hasToolCalls: false })
1070
+ this.emit('response', trimmed)
1071
+
1072
+ return trimmed
1073
+ }
1074
+
1075
+ /**
1076
+ * Runs the streaming Responses API loop. Handles local function calls by
1077
+ * executing handlers and submitting `function_call_output` items until
1078
+ * the model produces a final text response.
1079
+ */
1080
+ private async runResponsesLoop(context: {
1081
+ turn: number
1082
+ accumulated: string
1083
+ input: OpenAI.Responses.ResponseInput
1084
+ previousResponseId?: string
1085
+ }): Promise<string> {
1086
+
1087
+ const { turn } = context
1088
+ let accumulated = context.accumulated
1089
+ let turnContent = ''
1090
+ let finalResponse: OpenAI.Responses.Response | undefined
1091
+
1092
+ const toolsParam = this.responseTools.length > 0 ? this.responseTools : undefined
1093
+
1094
+ this.state.set('streaming', true)
1095
+ this.emit('turnStart', { turn, isFollowUp: turn > 1 })
1096
+
1097
+ const textFormat = this.structuredOutputConfig
1098
+ ? { text: { format: { type: 'json_schema' as const, ...this.structuredOutputConfig } } }
1099
+ : {}
1100
+
1101
+ try {
1102
+ const stream = await this.openai.raw.responses.create({
1103
+ model: this.model as OpenAI.Responses.ResponseCreateParams['model'],
1104
+ input: context.input,
1105
+ stream: true,
1106
+ previous_response_id: context.previousResponseId,
1107
+ ...(toolsParam ? { tools: toolsParam, tool_choice: 'auto', parallel_tool_calls: true } : {}),
1108
+ ...(this.responsesInstructions ? { instructions: this.responsesInstructions } : {}),
1109
+ ...(this.maxTokens ? { max_output_tokens: this.maxTokens } : {}),
1110
+ ...(this.state.get('temperature') != null ? { temperature: this.state.get('temperature') } : {}),
1111
+ ...(this.state.get('topP') != null ? { top_p: this.state.get('topP') } : {}),
1112
+ ...(this.state.get('topK') != null ? { top_k: this.state.get('topK') } : {}),
1113
+ ...(this.state.get('frequencyPenalty') != null ? { frequency_penalty: this.state.get('frequencyPenalty') } : {}),
1114
+ ...(this.state.get('presencePenalty') != null ? { presence_penalty: this.state.get('presencePenalty') } : {}),
1115
+ ...(this.state.get('stop') ? { stop: this.state.get('stop') } : {}),
1116
+ ...textFormat,
1117
+ }, { signal: this._abortController?.signal })
1118
+
1119
+ for await (const event of stream) {
1120
+ this.emit('rawEvent', event)
1121
+ if ((event as any).type?.startsWith?.('response.mcp_')) {
1122
+ this.emit('mcpEvent', event)
1123
+ }
1124
+ if (((event as any).type === 'response.output_item.added' || (event as any).type === 'response.output_item.done')
1125
+ && (event as any).item?.type?.startsWith?.('mcp_')) {
1126
+ this.emit('mcpEvent', event)
1127
+ }
1128
+
1129
+ if (event.type === 'response.output_text.delta') {
1130
+ const delta = event.delta || ''
1131
+ turnContent += delta
1132
+ accumulated += delta
1133
+ this.state.set('lastResponse', accumulated)
1134
+ this.emit('chunk', delta)
1135
+ this.emit('preview', accumulated)
1136
+ }
1137
+
1138
+ if (event.type === 'response.completed') {
1139
+ finalResponse = event.response
1140
+ this.emit('responseCompleted', event.response)
1141
+ }
1142
+ }
1143
+ } finally {
1144
+ this.state.set('streaming', false)
1145
+ }
1146
+
1147
+ if (!finalResponse) {
1148
+ throw new Error('Responses stream ended without a completed response')
1149
+ }
1150
+
1151
+ this.state.set('lastResponseId', finalResponse.id)
1152
+ this.applyResponsesUsage(finalResponse.usage || undefined)
1153
+
1154
+ const functionCalls = (finalResponse.output || []).filter((item) => item.type === 'function_call') as OpenAI.Responses.ResponseFunctionToolCall[]
1155
+ if (functionCalls.length > 0) {
1156
+ const assistantMessage: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = {
1157
+ role: 'assistant',
1158
+ content: turnContent || null,
1159
+ tool_calls: functionCalls.map((call) => ({
1160
+ id: call.call_id,
1161
+ type: 'function',
1162
+ function: {
1163
+ name: call.name,
1164
+ arguments: call.arguments || '{}',
1165
+ }
1166
+ }))
1167
+ }
1168
+ this.pushMessage(assistantMessage)
1169
+
1170
+ this.emit('toolCallsStart', functionCalls)
1171
+
1172
+ const functionOutputs: OpenAI.Responses.ResponseInputItem.FunctionCallOutput[] = []
1173
+ for (const call of functionCalls) {
1174
+ if (this._abortController?.signal.aborted) {
1175
+ throw new ConversationAbortError(accumulated)
1176
+ }
1177
+ const result = await this.executeTool(call.name, call.arguments || '{}')
1178
+
1179
+ this.pushMessage({
1180
+ role: 'tool',
1181
+ tool_call_id: call.call_id,
1182
+ content: result,
1183
+ })
1184
+
1185
+ functionOutputs.push({
1186
+ type: 'function_call_output',
1187
+ call_id: call.call_id,
1188
+ output: result,
1189
+ })
1190
+ }
1191
+
1192
+ this.emit('toolCallsEnd')
1193
+ this.emit('turnEnd', { turn, hasToolCalls: true })
1194
+
1195
+ return this.runResponsesLoop({
1196
+ turn: turn + 1,
1197
+ accumulated,
1198
+ input: functionOutputs,
1199
+ previousResponseId: finalResponse.id,
1200
+ })
1201
+ }
1202
+
1203
+ const finalText = turnContent || finalResponse.output_text || ''
1204
+ const assistantMessage: Message = { role: 'assistant', content: finalText }
1205
+ this.pushMessage(assistantMessage)
1206
+ this.state.set('lastResponse', accumulated || finalText)
1207
+
1208
+ this.emit('turnEnd', { turn, hasToolCalls: false })
1209
+ this.emit('response', accumulated || finalText)
1210
+
1211
+ return accumulated || finalText
1212
+ }
1213
+
1214
+ /** Recalculate the running cost estimate from current token usage and update state. */
1215
+ private updateCost() {
1216
+ const tokenUsage = this.state.get('tokenUsage')!
1217
+ const { inputCost, outputCost, totalCost } = calculateCost(this.model, tokenUsage.prompt, tokenUsage.completion, {
1218
+ cachedTokens: tokenUsage.cachedTokens,
1219
+ reasoningTokens: tokenUsage.reasoningTokens,
1220
+ })
1221
+ this.state.set('cost', { inputCost, outputCost, totalCost })
1222
+ }
1223
+
1224
+ /** Apply Responses API usage stats to this conversation's token usage counters. */
1225
+ private applyResponsesUsage(usage?: OpenAI.Responses.ResponseUsage) {
1226
+ if (!usage) return
1227
+ const prev = this.state.get('tokenUsage')!
1228
+ this.state.set('tokenUsage', {
1229
+ prompt: prev.prompt + (usage.input_tokens || 0),
1230
+ completion: prev.completion + (usage.output_tokens || 0),
1231
+ total: prev.total + (usage.total_tokens || 0),
1232
+ cachedTokens: prev.cachedTokens + (usage.input_tokens_details?.cached_tokens || 0),
1233
+ reasoningTokens: prev.reasoningTokens + (usage.output_tokens_details?.reasoning_tokens || 0),
1234
+ })
1235
+ this.updateCost()
1236
+ }
1237
+
1238
+ /**
1239
+ * Runs the streaming completion loop. If the model requests tool calls,
1240
+ * executes them and loops again until a text response is produced.
1241
+ *
1242
+ * @returns {Promise<string>} The final assistant text response
1243
+ */
1244
+ /**
1245
+ * Runs the streaming completion loop. If the model requests tool calls,
1246
+ * executes them and loops again until a text response is produced.
1247
+ *
1248
+ * @param context - Turn tracking: turn number and text accumulated across all turns
1249
+ * @returns {Promise<string>} The final assistant text response (accumulated across all turns)
1250
+ */
1251
+ private async runChatCompletionLoop(context: { turn: number; accumulated: string } = { turn: 1, accumulated: '' }): Promise<string> {
1252
+
1253
+ const { turn } = context
1254
+ let accumulated = context.accumulated
1255
+
1256
+ const hasTools = Object.keys(this.tools).length > 0
1257
+ const toolsParam = hasTools ? this.openaiTools : undefined
1258
+
1259
+ this.state.set('streaming', true)
1260
+ this.emit('turnStart', { turn, isFollowUp: turn > 1 })
1261
+
1262
+ let turnContent = ''
1263
+ let toolCalls: Array<{ id: string; function: { name: string; arguments: string }; type: 'function' }> = []
1264
+
1265
+ const responseFormat = this.structuredOutputConfig
1266
+ ? { response_format: { type: 'json_schema' as const, json_schema: this.structuredOutputConfig } }
1267
+ : {}
1268
+
1269
+ try {
1270
+ const stream = await this.openai.raw.chat.completions.create({
1271
+ model: this.model,
1272
+ messages: this.sanitizeMessages(this.getMessagesWithinBudget()),
1273
+ stream: true,
1274
+ ...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
1275
+ ...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
1276
+ ...(this.state.get('temperature') != null ? { temperature: this.state.get('temperature') } : {}),
1277
+ ...(this.state.get('topP') != null ? { top_p: this.state.get('topP') } : {}),
1278
+ ...(this.state.get('topK') != null ? { top_k: this.state.get('topK') } : {}),
1279
+ ...(this.state.get('frequencyPenalty') != null ? { frequency_penalty: this.state.get('frequencyPenalty') } : {}),
1280
+ ...(this.state.get('presencePenalty') != null ? { presence_penalty: this.state.get('presencePenalty') } : {}),
1281
+ ...(this.state.get('stop') ? { stop: this.state.get('stop') } : {}),
1282
+ ...responseFormat,
1283
+ }, { signal: this._abortController?.signal })
1284
+
1285
+ for await (const chunk of stream) {
1286
+ const delta = chunk.choices[0]?.delta
1287
+
1288
+ if (delta?.content) {
1289
+ turnContent += delta.content
1290
+ accumulated += delta.content
1291
+ this.state.set('lastResponse', accumulated)
1292
+ this.emit('chunk', delta.content)
1293
+ this.emit('preview', accumulated)
1294
+ }
1295
+
1296
+ if (delta?.tool_calls) {
1297
+ for (const tc of delta.tool_calls) {
1298
+ if (!toolCalls[tc.index]) {
1299
+ toolCalls[tc.index] = {
1300
+ id: tc.id || '',
1301
+ type: 'function',
1302
+ function: { name: '', arguments: '' }
1303
+ }
1304
+ }
1305
+ if (tc.id) {
1306
+ toolCalls[tc.index]!.id = tc.id
1307
+ }
1308
+ if (tc.function?.name) {
1309
+ toolCalls[tc.index]!.function.name += tc.function.name
1310
+ }
1311
+ if (tc.function?.arguments) {
1312
+ toolCalls[tc.index]!.function.arguments += tc.function.arguments
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ if (chunk.usage) {
1318
+ const prev = this.state.get('tokenUsage')!
1319
+ this.state.set('tokenUsage', {
1320
+ prompt: prev.prompt + (chunk.usage.prompt_tokens || 0),
1321
+ completion: prev.completion + (chunk.usage.completion_tokens || 0),
1322
+ total: prev.total + (chunk.usage.total_tokens || 0),
1323
+ cachedTokens: prev.cachedTokens + (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
1324
+ reasoningTokens: prev.reasoningTokens + (chunk.usage.completion_tokens_details?.reasoning_tokens || 0),
1325
+ })
1326
+ this.updateCost()
1327
+ }
1328
+ }
1329
+ } finally {
1330
+ this.state.set('streaming', false)
1331
+ }
1332
+
1333
+ // If the model produced tool calls, execute them and loop
1334
+ if (toolCalls.length > 0) {
1335
+ const assistantMessage: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = {
1336
+ role: 'assistant',
1337
+ content: turnContent || null,
1338
+ tool_calls: toolCalls
1339
+ }
1340
+ this.pushMessage(assistantMessage)
1341
+
1342
+ this.emit('toolCallsStart', toolCalls)
1343
+
1344
+ for (const tc of toolCalls) {
1345
+ if (this._abortController?.signal.aborted) {
1346
+ throw new ConversationAbortError(accumulated)
1347
+ }
1348
+ const result = await this.executeTool(tc.function.name, tc.function.arguments)
1349
+
1350
+ const toolMessage: OpenAI.Chat.Completions.ChatCompletionToolMessageParam = {
1351
+ role: 'tool',
1352
+ tool_call_id: tc.id,
1353
+ content: result
1354
+ }
1355
+ this.pushMessage(toolMessage)
1356
+ }
1357
+
1358
+ this.emit('toolCallsEnd')
1359
+ this.emit('turnEnd', { turn, hasToolCalls: true })
1360
+
1361
+ // Loop: let the model respond to tool results
1362
+ return this.runChatCompletionLoop({ turn: turn + 1, accumulated })
1363
+ }
1364
+
1365
+ // Final text response — use this turn's content for the message history,
1366
+ // but accumulated for the response event and return value
1367
+ const assistantMessage: Message = { role: 'assistant', content: turnContent }
1368
+ this.pushMessage(assistantMessage)
1369
+ this.state.set('lastResponse', accumulated)
1370
+
1371
+ this.emit('turnEnd', { turn, hasToolCalls: false })
1372
+ this.emit('response', accumulated)
1373
+
1374
+ return accumulated
1375
+ }
1376
+
1377
+ /**
1378
+ * Returns the messages array trimmed to fit within the maxInputTokens budget.
1379
+ * Keeps the system/developer message and drops oldest atomic groups first.
1380
+ *
1381
+ * Messages are grouped into atomic units so tool call/response pairs are never
1382
+ * split (which would cause a 400 from OpenAI):
1383
+ * - assistant with tool_calls + its subsequent tool response messages = one group
1384
+ * - standalone user, assistant (no tools), system = one group each
1385
+ *
1386
+ * If no maxInputTokens is set, returns messages as-is.
1387
+ */
1388
+ private getMessagesWithinBudget(): Message[] {
1389
+ const budget = resolveMaxInputTokens(this.options.maxInputTokens)
1390
+ if (!budget) return this.messages
1391
+
1392
+ const messages = this.messages
1393
+ if (messages.length === 0) return messages
1394
+
1395
+ // Check if the full history already fits
1396
+ const fullCount = countMessageTokens(messages, this.model)
1397
+ if (fullCount <= budget) return messages
1398
+
1399
+ // Separate system prompt from the rest
1400
+ const systemMsg = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
1401
+ ? messages[0]
1402
+ : null
1403
+ const nonSystem = systemMsg ? messages.slice(1) : [...messages]
1404
+
1405
+ // Group messages into atomic units.
1406
+ // An assistant message with tool_calls and its subsequent tool responses form one group.
1407
+ type MessageGroup = Message[]
1408
+ const groups: MessageGroup[] = []
1409
+ let i = 0
1410
+ while (i < nonSystem.length) {
1411
+ const msg = nonSystem[i]!
1412
+ if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
1413
+ // Collect the assistant + all following tool responses that belong to it
1414
+ const expectedIds = new Set(((msg as any).tool_calls as any[]).map((tc: any) => tc.id))
1415
+ const group: Message[] = [msg]
1416
+ let j = i + 1
1417
+ while (j < nonSystem.length && nonSystem[j]!.role === 'tool' && expectedIds.has((nonSystem[j] as any).tool_call_id)) {
1418
+ group.push(nonSystem[j]!)
1419
+ j++
1420
+ }
1421
+ groups.push(group)
1422
+ i = j
1423
+ } else {
1424
+ groups.push([msg])
1425
+ i++
1426
+ }
1427
+ }
1428
+
1429
+ // Walk backwards through groups, accumulating tokens until we exceed the budget
1430
+ const systemTokens = systemMsg ? countMessageTokens([systemMsg], this.model) : 0
1431
+ let running = systemTokens
1432
+ let cutoff = groups.length // start with nothing included
1433
+
1434
+ for (let g = groups.length - 1; g >= 0; g--) {
1435
+ const groupTokens = countMessageTokens(groups[g]!, this.model)
1436
+ if (running + groupTokens > budget) break
1437
+ running += groupTokens
1438
+ cutoff = g
1439
+ }
1440
+
1441
+ const kept = groups.slice(cutoff).flat()
1442
+ return systemMsg ? [systemMsg, ...kept] : kept
1443
+ }
1444
+
1445
+ private sanitizeMessages(messages: Message[]): Message[] {
1446
+ const result: Message[] = []
1447
+
1448
+ for (let i = 0; i < messages.length; i++) {
1449
+ const msg = messages[i]!
1450
+ result.push(msg)
1451
+
1452
+ // Check if this is an assistant message with tool_calls
1453
+ if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
1454
+ const toolCalls: Array<{ id: string }> = (msg as any).tool_calls
1455
+ const expectedIds = new Set(toolCalls.map(tc => tc.id))
1456
+
1457
+ // Scan forward for matching tool responses
1458
+ const foundIds = new Set<string>()
1459
+ for (let j = i + 1; j < messages.length; j++) {
1460
+ const next = messages[j]!
1461
+ if (next.role === 'tool' && expectedIds.has((next as any).tool_call_id)) {
1462
+ foundIds.add((next as any).tool_call_id)
1463
+ } else if (next.role !== 'tool') {
1464
+ break
1465
+ }
1466
+ }
1467
+
1468
+ // Add stub responses for any missing tool_call_ids
1469
+ for (const id of expectedIds) {
1470
+ if (!foundIds.has(id)) {
1471
+ result.push({
1472
+ role: 'tool',
1473
+ tool_call_id: id,
1474
+ content: '[tool execution was interrupted]',
1475
+ } as any)
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ return result
1482
+ }
1483
+
1484
+ /**
1485
+ * Append a message to the conversation state.
1486
+ *
1487
+ * @param {Message} message - The message to append
1488
+ */
1489
+ pushMessage(message: Message) {
1490
+ this.state.set('messages', [...this.messages, message])
1491
+ }
1492
+ }
1493
+
1494
+ export default Conversation