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