luca 2.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (532) hide show
  1. package/.github/workflows/release.yaml +170 -0
  2. package/AGENTS.md +99 -0
  3. package/CLAUDE.md +123 -0
  4. package/CNAME +1 -0
  5. package/README.md +275 -9
  6. package/RUNME.md +56 -0
  7. package/assistants/codingAssistant/ABOUT.md +5 -0
  8. package/assistants/codingAssistant/CORE.md +33 -0
  9. package/assistants/codingAssistant/hooks.ts +21 -0
  10. package/assistants/codingAssistant/tools.ts +12 -0
  11. package/assistants/inkbot/ABOUT.md +16 -0
  12. package/assistants/inkbot/CORE.md +330 -0
  13. package/assistants/inkbot/hooks.ts +6 -0
  14. package/assistants/inkbot/tools.ts +53 -0
  15. package/assistants/researcher/ABOUT.md +5 -0
  16. package/assistants/researcher/CORE.md +46 -0
  17. package/assistants/researcher/hooks.ts +16 -0
  18. package/assistants/researcher/tools.ts +237 -0
  19. package/bun.lock +2667 -0
  20. package/bunfig.toml +3 -0
  21. package/commands/audit-docs.ts +740 -0
  22. package/commands/build-bootstrap.ts +117 -0
  23. package/commands/build-python-bridge.ts +42 -0
  24. package/commands/build-scaffolds.ts +175 -0
  25. package/commands/bundle-consumer-project.ts +521 -0
  26. package/commands/generate-api-docs.ts +114 -0
  27. package/commands/inkbot.ts +874 -0
  28. package/commands/release.ts +80 -0
  29. package/commands/try-all-challenges.ts +543 -0
  30. package/commands/try-challenge.ts +100 -0
  31. package/dist/agi/container.server.d.ts +63 -0
  32. package/dist/agi/container.server.d.ts.map +1 -0
  33. package/dist/agi/endpoints/ask.d.ts +20 -0
  34. package/dist/agi/endpoints/ask.d.ts.map +1 -0
  35. package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
  36. package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
  37. package/dist/agi/endpoints/conversations.d.ts +18 -0
  38. package/dist/agi/endpoints/conversations.d.ts.map +1 -0
  39. package/dist/agi/endpoints/experts.d.ts +8 -0
  40. package/dist/agi/endpoints/experts.d.ts.map +1 -0
  41. package/dist/agi/feature.d.ts +9 -0
  42. package/dist/agi/feature.d.ts.map +1 -0
  43. package/dist/agi/features/assistant.d.ts +509 -0
  44. package/dist/agi/features/assistant.d.ts.map +1 -0
  45. package/dist/agi/features/assistants-manager.d.ts +236 -0
  46. package/dist/agi/features/assistants-manager.d.ts.map +1 -0
  47. package/dist/agi/features/autonomous-assistant.d.ts +281 -0
  48. package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
  49. package/dist/agi/features/browser-use.d.ts +479 -0
  50. package/dist/agi/features/browser-use.d.ts.map +1 -0
  51. package/dist/agi/features/claude-code.d.ts +824 -0
  52. package/dist/agi/features/claude-code.d.ts.map +1 -0
  53. package/dist/agi/features/conversation-history.d.ts +245 -0
  54. package/dist/agi/features/conversation-history.d.ts.map +1 -0
  55. package/dist/agi/features/conversation.d.ts +464 -0
  56. package/dist/agi/features/conversation.d.ts.map +1 -0
  57. package/dist/agi/features/docs-reader.d.ts +72 -0
  58. package/dist/agi/features/docs-reader.d.ts.map +1 -0
  59. package/dist/agi/features/file-tools.d.ts +110 -0
  60. package/dist/agi/features/file-tools.d.ts.map +1 -0
  61. package/dist/agi/features/luca-coder.d.ts +323 -0
  62. package/dist/agi/features/luca-coder.d.ts.map +1 -0
  63. package/dist/agi/features/openai-codex.d.ts +381 -0
  64. package/dist/agi/features/openai-codex.d.ts.map +1 -0
  65. package/dist/agi/features/openapi.d.ts +200 -0
  66. package/dist/agi/features/openapi.d.ts.map +1 -0
  67. package/dist/agi/features/skills-library.d.ts +167 -0
  68. package/dist/agi/features/skills-library.d.ts.map +1 -0
  69. package/dist/agi/index.d.ts +5 -0
  70. package/dist/agi/index.d.ts.map +1 -0
  71. package/dist/agi/lib/interceptor-chain.d.ts +44 -0
  72. package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
  73. package/dist/agi/lib/token-counter.d.ts +13 -0
  74. package/dist/agi/lib/token-counter.d.ts.map +1 -0
  75. package/dist/bootstrap/generated.d.ts +5 -0
  76. package/dist/bootstrap/generated.d.ts.map +1 -0
  77. package/dist/browser.d.ts +12 -0
  78. package/dist/browser.d.ts.map +1 -0
  79. package/dist/bus.d.ts +29 -0
  80. package/dist/bus.d.ts.map +1 -0
  81. package/dist/cli/build-info.d.ts +4 -0
  82. package/dist/cli/build-info.d.ts.map +1 -0
  83. package/dist/cli/cli.d.ts +3 -12
  84. package/dist/cli/cli.d.ts.map +1 -0
  85. package/dist/client.d.ts +60 -0
  86. package/dist/client.d.ts.map +1 -0
  87. package/dist/clients/civitai/index.d.ts +472 -0
  88. package/dist/clients/civitai/index.d.ts.map +1 -0
  89. package/dist/clients/client-template.d.ts +30 -0
  90. package/dist/clients/client-template.d.ts.map +1 -0
  91. package/dist/clients/comfyui/index.d.ts +281 -0
  92. package/dist/clients/comfyui/index.d.ts.map +1 -0
  93. package/dist/clients/elevenlabs/index.d.ts +197 -0
  94. package/dist/clients/elevenlabs/index.d.ts.map +1 -0
  95. package/dist/clients/graph.d.ts +64 -0
  96. package/dist/clients/graph.d.ts.map +1 -0
  97. package/dist/clients/openai/index.d.ts +247 -0
  98. package/dist/clients/openai/index.d.ts.map +1 -0
  99. package/dist/clients/rest.d.ts +92 -0
  100. package/dist/clients/rest.d.ts.map +1 -0
  101. package/dist/clients/supabase/index.d.ts +176 -0
  102. package/dist/clients/supabase/index.d.ts.map +1 -0
  103. package/dist/clients/websocket.d.ts +127 -0
  104. package/dist/clients/websocket.d.ts.map +1 -0
  105. package/dist/command.d.ts +163 -0
  106. package/dist/command.d.ts.map +1 -0
  107. package/dist/commands/bootstrap.d.ts +20 -0
  108. package/dist/commands/bootstrap.d.ts.map +1 -0
  109. package/dist/commands/chat.d.ts +37 -0
  110. package/dist/commands/chat.d.ts.map +1 -0
  111. package/dist/commands/code.d.ts +28 -0
  112. package/dist/commands/code.d.ts.map +1 -0
  113. package/dist/commands/console.d.ts +22 -0
  114. package/dist/commands/console.d.ts.map +1 -0
  115. package/dist/commands/describe.d.ts +50 -0
  116. package/dist/commands/describe.d.ts.map +1 -0
  117. package/dist/commands/eval.d.ts +23 -0
  118. package/dist/commands/eval.d.ts.map +1 -0
  119. package/dist/commands/help.d.ts +25 -0
  120. package/dist/commands/help.d.ts.map +1 -0
  121. package/dist/commands/index.d.ts +18 -0
  122. package/dist/commands/index.d.ts.map +1 -0
  123. package/dist/commands/introspect.d.ts +24 -0
  124. package/dist/commands/introspect.d.ts.map +1 -0
  125. package/dist/commands/mcp.d.ts +35 -0
  126. package/dist/commands/mcp.d.ts.map +1 -0
  127. package/dist/commands/prompt.d.ts +38 -0
  128. package/dist/commands/prompt.d.ts.map +1 -0
  129. package/dist/commands/run.d.ts +24 -0
  130. package/dist/commands/run.d.ts.map +1 -0
  131. package/dist/commands/sandbox-mcp.d.ts +34 -0
  132. package/dist/commands/sandbox-mcp.d.ts.map +1 -0
  133. package/dist/commands/save-api-docs.d.ts +21 -0
  134. package/dist/commands/save-api-docs.d.ts.map +1 -0
  135. package/dist/commands/scaffold.d.ts +24 -0
  136. package/dist/commands/scaffold.d.ts.map +1 -0
  137. package/dist/commands/select.d.ts +22 -0
  138. package/dist/commands/select.d.ts.map +1 -0
  139. package/dist/commands/serve.d.ts +29 -0
  140. package/dist/commands/serve.d.ts.map +1 -0
  141. package/dist/container-describer.d.ts +144 -0
  142. package/dist/container-describer.d.ts.map +1 -0
  143. package/dist/container.d.ts +451 -0
  144. package/dist/container.d.ts.map +1 -0
  145. package/dist/endpoint.d.ts +113 -0
  146. package/dist/endpoint.d.ts.map +1 -0
  147. package/dist/feature.d.ts +47 -0
  148. package/dist/feature.d.ts.map +1 -0
  149. package/dist/graft.d.ts +29 -0
  150. package/dist/graft.d.ts.map +1 -0
  151. package/dist/hash-object.d.ts +8 -0
  152. package/dist/hash-object.d.ts.map +1 -0
  153. package/dist/helper.d.ts +209 -0
  154. package/dist/helper.d.ts.map +1 -0
  155. package/dist/introspection/generated.node.d.ts +44623 -0
  156. package/dist/introspection/generated.node.d.ts.map +1 -0
  157. package/dist/introspection/generated.web.d.ts +1412 -0
  158. package/dist/introspection/generated.web.d.ts.map +1 -0
  159. package/dist/introspection/index.d.ts +156 -0
  160. package/dist/introspection/index.d.ts.map +1 -0
  161. package/dist/introspection/scan.d.ts +147 -0
  162. package/dist/introspection/scan.d.ts.map +1 -0
  163. package/dist/node/container.d.ts +256 -0
  164. package/dist/node/container.d.ts.map +1 -0
  165. package/dist/node/feature.d.ts +9 -0
  166. package/dist/node/feature.d.ts.map +1 -0
  167. package/dist/node/features/container-link.d.ts +213 -0
  168. package/dist/node/features/container-link.d.ts.map +1 -0
  169. package/dist/node/features/content-db.d.ts +354 -0
  170. package/dist/node/features/content-db.d.ts.map +1 -0
  171. package/dist/node/features/disk-cache.d.ts +236 -0
  172. package/dist/node/features/disk-cache.d.ts.map +1 -0
  173. package/dist/node/features/dns.d.ts +511 -0
  174. package/dist/node/features/dns.d.ts.map +1 -0
  175. package/dist/node/features/docker.d.ts +485 -0
  176. package/dist/node/features/docker.d.ts.map +1 -0
  177. package/dist/node/features/downloader.d.ts +73 -0
  178. package/dist/node/features/downloader.d.ts.map +1 -0
  179. package/dist/node/features/figlet-fonts.d.ts +4 -0
  180. package/dist/node/features/figlet-fonts.d.ts.map +1 -0
  181. package/dist/node/features/file-manager.d.ts +177 -0
  182. package/dist/node/features/file-manager.d.ts.map +1 -0
  183. package/dist/node/features/fs.d.ts +635 -0
  184. package/dist/node/features/fs.d.ts.map +1 -0
  185. package/dist/node/features/git.d.ts +329 -0
  186. package/dist/node/features/git.d.ts.map +1 -0
  187. package/dist/node/features/google-auth.d.ts +200 -0
  188. package/dist/node/features/google-auth.d.ts.map +1 -0
  189. package/dist/node/features/google-calendar.d.ts +194 -0
  190. package/dist/node/features/google-calendar.d.ts.map +1 -0
  191. package/dist/node/features/google-docs.d.ts +138 -0
  192. package/dist/node/features/google-docs.d.ts.map +1 -0
  193. package/dist/node/features/google-drive.d.ts +202 -0
  194. package/dist/node/features/google-drive.d.ts.map +1 -0
  195. package/dist/node/features/google-mail.d.ts +221 -0
  196. package/dist/node/features/google-mail.d.ts.map +1 -0
  197. package/dist/node/features/google-sheets.d.ts +157 -0
  198. package/dist/node/features/google-sheets.d.ts.map +1 -0
  199. package/dist/node/features/grep.d.ts +207 -0
  200. package/dist/node/features/grep.d.ts.map +1 -0
  201. package/dist/node/features/helpers.d.ts +236 -0
  202. package/dist/node/features/helpers.d.ts.map +1 -0
  203. package/dist/node/features/ink.d.ts +332 -0
  204. package/dist/node/features/ink.d.ts.map +1 -0
  205. package/dist/node/features/ipc-socket.d.ts +298 -0
  206. package/dist/node/features/ipc-socket.d.ts.map +1 -0
  207. package/dist/node/features/json-tree.d.ts +140 -0
  208. package/dist/node/features/json-tree.d.ts.map +1 -0
  209. package/dist/node/features/networking.d.ts +373 -0
  210. package/dist/node/features/networking.d.ts.map +1 -0
  211. package/dist/node/features/nlp.d.ts +125 -0
  212. package/dist/node/features/nlp.d.ts.map +1 -0
  213. package/dist/node/features/opener.d.ts +93 -0
  214. package/dist/node/features/opener.d.ts.map +1 -0
  215. package/dist/node/features/os.d.ts +168 -0
  216. package/dist/node/features/os.d.ts.map +1 -0
  217. package/dist/node/features/package-finder.d.ts +419 -0
  218. package/dist/node/features/package-finder.d.ts.map +1 -0
  219. package/dist/node/features/postgres.d.ts +173 -0
  220. package/dist/node/features/postgres.d.ts.map +1 -0
  221. package/dist/node/features/proc.d.ts +285 -0
  222. package/dist/node/features/proc.d.ts.map +1 -0
  223. package/dist/node/features/process-manager.d.ts +427 -0
  224. package/dist/node/features/process-manager.d.ts.map +1 -0
  225. package/dist/node/features/python.d.ts +477 -0
  226. package/dist/node/features/python.d.ts.map +1 -0
  227. package/dist/node/features/redis.d.ts +247 -0
  228. package/dist/node/features/redis.d.ts.map +1 -0
  229. package/dist/node/features/repl.d.ts +84 -0
  230. package/dist/node/features/repl.d.ts.map +1 -0
  231. package/dist/node/features/runpod.d.ts +527 -0
  232. package/dist/node/features/runpod.d.ts.map +1 -0
  233. package/dist/node/features/secure-shell.d.ts +145 -0
  234. package/dist/node/features/secure-shell.d.ts.map +1 -0
  235. package/dist/node/features/semantic-search.d.ts +207 -0
  236. package/dist/node/features/semantic-search.d.ts.map +1 -0
  237. package/dist/node/features/sqlite.d.ts +180 -0
  238. package/dist/node/features/sqlite.d.ts.map +1 -0
  239. package/dist/node/features/telegram.d.ts +173 -0
  240. package/dist/node/features/telegram.d.ts.map +1 -0
  241. package/dist/node/features/transpiler.d.ts +51 -0
  242. package/dist/node/features/transpiler.d.ts.map +1 -0
  243. package/dist/node/features/tts.d.ts +108 -0
  244. package/dist/node/features/tts.d.ts.map +1 -0
  245. package/dist/node/features/ui.d.ts +562 -0
  246. package/dist/node/features/ui.d.ts.map +1 -0
  247. package/dist/node/features/vault.d.ts +90 -0
  248. package/dist/node/features/vault.d.ts.map +1 -0
  249. package/dist/node/features/vm.d.ts +285 -0
  250. package/dist/node/features/vm.d.ts.map +1 -0
  251. package/dist/node/features/yaml-tree.d.ts +118 -0
  252. package/dist/node/features/yaml-tree.d.ts.map +1 -0
  253. package/dist/node/features/yaml.d.ts +127 -0
  254. package/dist/node/features/yaml.d.ts.map +1 -0
  255. package/dist/node.d.ts +67 -0
  256. package/dist/node.d.ts.map +1 -0
  257. package/dist/python/generated.d.ts +2 -0
  258. package/dist/python/generated.d.ts.map +1 -0
  259. package/dist/react/index.d.ts +36 -0
  260. package/dist/react/index.d.ts.map +1 -0
  261. package/dist/registry.d.ts +97 -0
  262. package/dist/registry.d.ts.map +1 -0
  263. package/dist/scaffolds/generated.d.ts +13 -0
  264. package/dist/scaffolds/generated.d.ts.map +1 -0
  265. package/dist/scaffolds/template.d.ts +11 -0
  266. package/dist/scaffolds/template.d.ts.map +1 -0
  267. package/dist/schemas/base.d.ts +254 -0
  268. package/dist/schemas/base.d.ts.map +1 -0
  269. package/dist/selector.d.ts +130 -0
  270. package/dist/selector.d.ts.map +1 -0
  271. package/dist/server.d.ts +89 -0
  272. package/dist/server.d.ts.map +1 -0
  273. package/dist/servers/express.d.ts +104 -0
  274. package/dist/servers/express.d.ts.map +1 -0
  275. package/dist/servers/mcp.d.ts +201 -0
  276. package/dist/servers/mcp.d.ts.map +1 -0
  277. package/dist/servers/socket.d.ts +121 -0
  278. package/dist/servers/socket.d.ts.map +1 -0
  279. package/dist/state.d.ts +24 -0
  280. package/dist/state.d.ts.map +1 -0
  281. package/dist/web/clients/socket.d.ts +37 -0
  282. package/dist/web/clients/socket.d.ts.map +1 -0
  283. package/dist/web/container.d.ts +55 -0
  284. package/dist/web/container.d.ts.map +1 -0
  285. package/dist/web/extension.d.ts +4 -0
  286. package/dist/web/extension.d.ts.map +1 -0
  287. package/dist/web/feature.d.ts +8 -0
  288. package/dist/web/feature.d.ts.map +1 -0
  289. package/dist/web/features/asset-loader.d.ts +35 -0
  290. package/dist/web/features/asset-loader.d.ts.map +1 -0
  291. package/dist/web/features/container-link.d.ts +167 -0
  292. package/dist/web/features/container-link.d.ts.map +1 -0
  293. package/dist/web/features/esbuild.d.ts +51 -0
  294. package/dist/web/features/esbuild.d.ts.map +1 -0
  295. package/dist/web/features/helpers.d.ts +140 -0
  296. package/dist/web/features/helpers.d.ts.map +1 -0
  297. package/dist/web/features/network.d.ts +69 -0
  298. package/dist/web/features/network.d.ts.map +1 -0
  299. package/dist/web/features/speech.d.ts +71 -0
  300. package/dist/web/features/speech.d.ts.map +1 -0
  301. package/dist/web/features/vault.d.ts +62 -0
  302. package/dist/web/features/vault.d.ts.map +1 -0
  303. package/dist/web/features/vm.d.ts +48 -0
  304. package/dist/web/features/vm.d.ts.map +1 -0
  305. package/dist/web/features/voice-recognition.d.ts +96 -0
  306. package/dist/web/features/voice-recognition.d.ts.map +1 -0
  307. package/dist/web/shims/isomorphic-vm.d.ts +22 -0
  308. package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
  309. package/index.html +1457 -0
  310. package/index.ts +1 -0
  311. package/install.sh +84 -0
  312. package/luca.cli.ts +16 -0
  313. package/luca.console.ts +9 -0
  314. package/main.py +6 -0
  315. package/package.json +219 -58
  316. package/public/index.html +1457 -0
  317. package/public/slides-ai-native.html +902 -0
  318. package/public/slides-intro.html +974 -0
  319. package/pyproject.toml +7 -0
  320. package/scripts/build-web.ts +28 -0
  321. package/scripts/examples/ask-luca-expert.ts +42 -0
  322. package/scripts/examples/assistant-questions.ts +12 -0
  323. package/scripts/examples/excalidraw-expert.ts +75 -0
  324. package/scripts/examples/expert-chat.ts +0 -0
  325. package/scripts/examples/file-manager.ts +14 -0
  326. package/scripts/examples/ideas.ts +12 -0
  327. package/scripts/examples/interactive-chat.ts +20 -0
  328. package/scripts/examples/openai-tool-calls.ts +113 -0
  329. package/scripts/examples/opening-a-web-browser.ts +5 -0
  330. package/scripts/examples/telegram-bot.ts +79 -0
  331. package/scripts/examples/using-assistant-with-mcp.ts +555 -0
  332. package/scripts/examples/using-claude-code.ts +10 -0
  333. package/scripts/examples/using-contentdb.ts +35 -0
  334. package/scripts/examples/using-conversations.ts +35 -0
  335. package/scripts/examples/using-disk-cache.ts +10 -0
  336. package/scripts/examples/using-docker-shell.ts +75 -0
  337. package/scripts/examples/using-elevenlabs.ts +25 -0
  338. package/scripts/examples/using-google-calendar.ts +57 -0
  339. package/scripts/examples/using-google-docs.ts +74 -0
  340. package/scripts/examples/using-google-drive.ts +74 -0
  341. package/scripts/examples/using-google-sheets.ts +89 -0
  342. package/scripts/examples/using-nlp.ts +55 -0
  343. package/scripts/examples/using-ollama.ts +11 -0
  344. package/scripts/examples/using-postgres.ts +55 -0
  345. package/scripts/examples/using-runpod.ts +32 -0
  346. package/scripts/examples/using-tts.ts +40 -0
  347. package/scripts/scaffold.ts +391 -0
  348. package/scripts/scratch.ts +15 -0
  349. package/scripts/stamp-build.sh +12 -0
  350. package/scripts/test-assistant-hooks.ts +13 -0
  351. package/scripts/test-docs-reader.ts +10 -0
  352. package/scripts/test-linux-binary.sh +80 -0
  353. package/scripts/update-introspection-data.ts +58 -0
  354. package/src/agi/README.md +14 -0
  355. package/src/agi/container.server.ts +156 -0
  356. package/src/agi/feature.ts +13 -0
  357. package/src/agi/features/agent-memory.ts +694 -0
  358. package/src/agi/features/assistant.ts +1653 -0
  359. package/src/agi/features/assistants-manager.ts +534 -0
  360. package/src/agi/features/autonomous-assistant.ts +431 -0
  361. package/src/agi/features/browser-use.ts +672 -0
  362. package/src/agi/features/claude-code.ts +1584 -0
  363. package/src/agi/features/coding-tools.ts +175 -0
  364. package/src/agi/features/conversation-history.ts +672 -0
  365. package/src/agi/features/conversation.ts +1494 -0
  366. package/src/agi/features/docs-reader.ts +167 -0
  367. package/src/agi/features/file-tools.ts +340 -0
  368. package/src/agi/features/luca-coder.ts +641 -0
  369. package/src/agi/features/mcp-bridge.ts +532 -0
  370. package/src/agi/features/openai-codex.ts +651 -0
  371. package/src/agi/features/openapi.ts +445 -0
  372. package/src/agi/features/skills-library.ts +557 -0
  373. package/src/agi/index.ts +6 -0
  374. package/src/agi/lib/interceptor-chain.ts +89 -0
  375. package/src/agi/lib/token-counter.ts +202 -0
  376. package/src/bootstrap/generated.ts +9791 -0
  377. package/src/browser.ts +25 -0
  378. package/src/bus.ts +122 -0
  379. package/src/cli/build-info.ts +4 -0
  380. package/src/cli/cli.ts +355 -0
  381. package/src/client.ts +170 -0
  382. package/src/clients/civitai/index.ts +537 -0
  383. package/src/clients/client-template.ts +41 -0
  384. package/src/clients/comfyui/index.ts +604 -0
  385. package/src/clients/elevenlabs/index.ts +317 -0
  386. package/src/clients/graph.ts +87 -0
  387. package/src/clients/openai/index.ts +456 -0
  388. package/src/clients/rest.ts +207 -0
  389. package/src/clients/supabase/index.ts +357 -0
  390. package/src/clients/voicebox/index.ts +300 -0
  391. package/src/clients/websocket.ts +251 -0
  392. package/src/command.ts +506 -0
  393. package/src/commands/bootstrap.ts +244 -0
  394. package/src/commands/chat.ts +309 -0
  395. package/src/commands/code.ts +371 -0
  396. package/src/commands/console.ts +189 -0
  397. package/src/commands/describe.ts +243 -0
  398. package/src/commands/eval.ts +67 -0
  399. package/src/commands/help.ts +240 -0
  400. package/src/commands/index.ts +19 -0
  401. package/src/commands/introspect.ts +218 -0
  402. package/src/commands/mcp.ts +64 -0
  403. package/src/commands/prompt.ts +1014 -0
  404. package/src/commands/run.ts +278 -0
  405. package/src/commands/sandbox-mcp.ts +343 -0
  406. package/src/commands/save-api-docs.ts +51 -0
  407. package/src/commands/scaffold.ts +225 -0
  408. package/src/commands/select.ts +99 -0
  409. package/src/commands/serve.ts +208 -0
  410. package/src/container-describer.ts +1091 -0
  411. package/src/container.ts +1199 -0
  412. package/src/endpoint.ts +365 -0
  413. package/src/entity.ts +173 -0
  414. package/src/feature.ts +118 -0
  415. package/src/graft.ts +181 -0
  416. package/src/hash-object.ts +97 -0
  417. package/src/helper.ts +849 -0
  418. package/src/introspection/generated.agi.ts +41200 -0
  419. package/src/introspection/generated.node.ts +28773 -0
  420. package/src/introspection/generated.web.ts +2272 -0
  421. package/src/introspection/index.ts +296 -0
  422. package/src/introspection/scan.ts +1136 -0
  423. package/src/node/container.ts +409 -0
  424. package/src/node/feature.ts +13 -0
  425. package/src/node/features/container-link.ts +559 -0
  426. package/src/node/features/content-db.ts +849 -0
  427. package/src/node/features/disk-cache.ts +388 -0
  428. package/src/node/features/display-result.ts +57 -0
  429. package/src/node/features/dns.ts +669 -0
  430. package/src/node/features/docker.ts +921 -0
  431. package/src/node/features/downloader.ts +79 -0
  432. package/src/node/features/figlet-fonts.ts +600 -0
  433. package/src/node/features/file-manager.ts +535 -0
  434. package/src/node/features/fs.ts +1050 -0
  435. package/src/node/features/git.ts +592 -0
  436. package/src/node/features/google-auth.ts +504 -0
  437. package/src/node/features/google-calendar.ts +306 -0
  438. package/src/node/features/google-docs.ts +412 -0
  439. package/src/node/features/google-drive.ts +346 -0
  440. package/src/node/features/google-mail.ts +540 -0
  441. package/src/node/features/google-sheets.ts +286 -0
  442. package/src/node/features/grep.ts +427 -0
  443. package/src/node/features/helpers.ts +762 -0
  444. package/src/node/features/ink.ts +490 -0
  445. package/src/node/features/ipc-socket.ts +649 -0
  446. package/src/node/features/json-tree.ts +170 -0
  447. package/src/node/features/networking.ts +961 -0
  448. package/src/node/features/nlp.ts +212 -0
  449. package/src/node/features/opener.ts +180 -0
  450. package/src/node/features/os.ts +403 -0
  451. package/src/node/features/package-finder.ts +540 -0
  452. package/src/node/features/postgres.ts +289 -0
  453. package/src/node/features/proc.ts +503 -0
  454. package/src/node/features/process-manager.ts +844 -0
  455. package/src/node/features/python.ts +912 -0
  456. package/src/node/features/redis.ts +446 -0
  457. package/src/node/features/repl.ts +212 -0
  458. package/src/node/features/runpod.ts +811 -0
  459. package/src/node/features/secure-shell.ts +261 -0
  460. package/src/node/features/semantic-search.ts +935 -0
  461. package/src/node/features/sqlite.ts +289 -0
  462. package/src/node/features/telegram.ts +343 -0
  463. package/src/node/features/transpiler.ts +160 -0
  464. package/src/node/features/tts.ts +185 -0
  465. package/src/node/features/ui.ts +791 -0
  466. package/src/node/features/vault.ts +153 -0
  467. package/src/node/features/vm.ts +462 -0
  468. package/src/node/features/yaml-tree.ts +148 -0
  469. package/src/node/features/yaml.ts +133 -0
  470. package/src/node.ts +76 -0
  471. package/src/python/bridge.py +220 -0
  472. package/src/python/generated.ts +226 -0
  473. package/src/react/index.ts +175 -0
  474. package/src/registry.ts +210 -0
  475. package/src/scaffolds/generated.ts +1814 -0
  476. package/src/scaffolds/template.ts +46 -0
  477. package/src/schemas/base.ts +296 -0
  478. package/src/selector.ts +352 -0
  479. package/src/server.ts +229 -0
  480. package/src/servers/express.ts +283 -0
  481. package/src/servers/mcp.ts +802 -0
  482. package/src/servers/socket.ts +258 -0
  483. package/src/state.ts +101 -0
  484. package/src/web/clients/socket.ts +99 -0
  485. package/src/web/container.ts +75 -0
  486. package/src/web/extension.ts +30 -0
  487. package/src/web/feature.ts +12 -0
  488. package/src/web/features/asset-loader.ts +72 -0
  489. package/src/web/features/container-link.ts +382 -0
  490. package/src/web/features/esbuild.ts +93 -0
  491. package/src/web/features/helpers.ts +291 -0
  492. package/src/web/features/network.ts +85 -0
  493. package/src/web/features/speech.ts +104 -0
  494. package/src/web/features/vault.ts +207 -0
  495. package/src/web/features/vm.ts +85 -0
  496. package/src/web/features/voice-recognition.ts +161 -0
  497. package/src/web/shims/isomorphic-vm.ts +149 -0
  498. package/tsconfig.build.json +12 -0
  499. package/tsconfig.json +58 -0
  500. package/uv.lock +8 -0
  501. package/LICENSE +0 -21
  502. package/dist/cli/cli.js +0 -48
  503. package/dist/cli/common.d.ts +0 -2
  504. package/dist/cli/common.js +0 -6
  505. package/dist/cli/index.d.ts +0 -2
  506. package/dist/cli/index.js +0 -5
  507. package/dist/cli/run.d.ts +0 -1
  508. package/dist/cli/run.js +0 -38
  509. package/dist/core/index.d.ts +0 -4
  510. package/dist/core/index.js +0 -32
  511. package/dist/core/read.d.ts +0 -2
  512. package/dist/core/read.js +0 -29
  513. package/dist/core/request.d.ts +0 -1
  514. package/dist/core/request.js +0 -2
  515. package/dist/core/write.d.ts +0 -2
  516. package/dist/core/write.js +0 -21
  517. package/dist/index.d.ts +0 -1
  518. package/dist/index.js +0 -5
  519. package/dist/utils/common.d.ts +0 -9
  520. package/dist/utils/common.js +0 -57
  521. package/dist/utils/consts.d.ts +0 -3
  522. package/dist/utils/consts.js +0 -11
  523. package/dist/utils/dict.d.ts +0 -1
  524. package/dist/utils/dict.js +0 -7
  525. package/dist/utils/index.d.ts +0 -5
  526. package/dist/utils/index.js +0 -21
  527. package/dist/utils/log.d.ts +0 -1
  528. package/dist/utils/log.js +0 -5
  529. package/dist/utils/types.d.ts +0 -1
  530. package/dist/utils/types.js +0 -2
  531. package/dist/utils/utils.test.d.ts +0 -1
  532. package/dist/utils/utils.test.js +0 -7
@@ -0,0 +1,1653 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { type AvailableFeatures } from 'luca/feature'
4
+ import { Feature } from '../feature.js'
5
+ import type { Conversation, ConversationTool, ContentPart, AskOptions, ForkOptions, Message } from './conversation'
6
+ import type { ContentDb } from 'luca/node'
7
+ import type { ConversationHistory, ConversationMeta } from './conversation-history'
8
+ import hashObject from '../../hash-object.js'
9
+ import { InterceptorChain, type InterceptorFn, type InterceptorPoints, type InterceptorPoint } from '../lib/interceptor-chain.js'
10
+ import type { Entity } from '../../entity.js'
11
+ import { State } from '../../state.js'
12
+
13
+ declare module 'luca/feature' {
14
+ interface AvailableFeatures {
15
+ assistant: typeof Assistant
16
+ }
17
+ }
18
+
19
+ export const AssistantEventsSchema = FeatureEventsSchema.extend({
20
+ created: z.tuple([]).describe('Emitted immediately after the assistant loads its prompt, tools, and hooks.'),
21
+ started: z.tuple([]).describe('Emitted when the assistant has been initialized'),
22
+ turnStart: z.tuple([z.object({ turn: z.number(), isFollowUp: z.boolean() })]).describe('Emitted when a new completion turn begins. isFollowUp is true when resuming after tool calls'),
23
+ turnEnd: z.tuple([z.object({ turn: z.number(), hasToolCalls: z.boolean() })]).describe('Emitted when a completion turn ends. hasToolCalls indicates whether tool calls will follow'),
24
+ chunk: z.tuple([z.string().describe('A chunk of streamed text')]).describe('Emitted as tokens stream in'),
25
+ preview: z.tuple([z.string().describe('The accumulated response so far')]).describe('Emitted with the full response text accumulated across all turns'),
26
+ response: z.tuple([z.string().describe('The final response text')]).describe('Emitted when a complete response is produced (accumulated across all turns)'),
27
+ rawEvent: z.tuple([z.any().describe('A raw streaming event from the active model API')]).describe('Emitted for each raw streaming event from the underlying conversation transport'),
28
+ mcpEvent: z.tuple([z.any().describe('A raw MCP-related streaming event')]).describe('Emitted for MCP-specific streaming and output-item events when using Responses API MCP tools'),
29
+ toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Tool arguments')]).describe('Emitted when a tool is called'),
30
+ toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
31
+ toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
32
+ hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
33
+ reloaded: z.tuple([]).describe('Emitted after tools, hooks, and system prompt are reloaded from disk'),
34
+ systemPromptExtensionsChanged: z.tuple([]).describe('Emitted when system prompt extensions are added or removed'),
35
+ })
36
+
37
+ export const AssistantStateSchema = FeatureStateSchema.extend({
38
+ started: z.boolean().describe('Whether the assistant has been initialized'),
39
+ conversationCount: z.number().describe('Number of ask() calls made'),
40
+ lastResponse: z.string().describe('The most recent response text'),
41
+ folder: z.string().describe('The resolved assistant folder path'),
42
+ docsFolder: z.string().describe('The resolved docs folder'),
43
+ conversationId: z.string().optional().describe('The active conversation persistence ID'),
44
+ threadId: z.string().optional().describe('The active thread ID'),
45
+ systemPrompt: z.string().describe('The loaded system prompt text'),
46
+ systemPromptExtensions: z.record(z.string(), z.string()).describe('Named extensions appended to the system prompt'),
47
+ meta: z.record(z.string(), z.any()).describe('Parsed YAML frontmatter from CORE.md'),
48
+ tools: z.record(z.string(), z.any()).describe('Registered tool implementations'),
49
+ hooks: z.record(z.string(), z.any()).describe('Loaded event hook functions'),
50
+ resumeThreadId: z.string().optional().describe('Thread ID override for resume'),
51
+ pendingPlugins: z.array(z.any()).describe('Pending async plugin promises'),
52
+ conversation: z.any().nullable().describe('The active Conversation feature instance'),
53
+ subagents: z.record(z.string(), z.any()).describe('Cached subagent instances'),
54
+ forkDepth: z.number().describe('How many times this assistant has been forked from an ancestor. 0 = original.'),
55
+ })
56
+
57
+ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
58
+ /** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts). Optional for runtime-created assistants. */
59
+ folder: z.string().default('.').describe('The folder containing the assistant definition. Defaults to cwd for runtime-created assistants.'),
60
+
61
+ /** If the docs folder is different from folder/docs */
62
+ docsFolder: z.string().optional().describe('The folder containing the assistant documentation'),
63
+
64
+ /** Provide a complete system prompt directly, bypassing CORE.md. Useful for runtime-created assistants. */
65
+ systemPrompt: z.string().optional().describe('Provide a complete system prompt directly, bypassing CORE.md'),
66
+
67
+ /** Text to prepend to the system prompt from CORE.md */
68
+ prependPrompt: z.string().optional().describe('Text to prepend to the system prompt'),
69
+
70
+ /** Text to append to the system prompt from CORE.md */
71
+ appendPrompt: z.string().optional().describe('Text to append to the system prompt'),
72
+ /** Override or extend the tools loaded from tools.ts */
73
+
74
+ tools: z.record(z.string(), z.any()).optional().describe('Override or extend the tools loaded from tools.ts'),
75
+ /** Override or extend the schemas loaded from tools.ts */
76
+
77
+ schemas: z.record(z.string(), z.any()).optional().describe('Override or extend schemas whose keys match tool names'),
78
+ /** OpenAI model to use for the conversation */
79
+
80
+ model: z.string().optional().describe('OpenAI model to use'),
81
+ /** Maximum number of output tokens per completion */
82
+
83
+ maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
84
+ /** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
85
+ temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2)'),
86
+ /** Nucleus sampling cutoff (0-1). */
87
+ topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1)'),
88
+ /** Top-K sampling. Only supported by local/Anthropic models. */
89
+ topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
90
+ /** Frequency penalty (-2 to 2). */
91
+ frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2)'),
92
+ /** Presence penalty (-2 to 2). */
93
+ presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2)'),
94
+ /** Stop sequences. */
95
+ stop: z.array(z.string()).optional().describe('Stop sequences'),
96
+
97
+ local: z.boolean().default(false).describe('Whether to use our local models for this'),
98
+
99
+ /** History persistence mode: lifecycle (ephemeral), daily (auto-resume per day), persistent (single long-running thread), session (unique per run, resumable) */
100
+ historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Conversation history persistence mode'),
101
+
102
+ /** When true, prepend a timestamp to each user message so the assistant can track the passage of time across sessions */
103
+ injectTimestamps: z.boolean().default(false).describe('Prepend timestamps to user messages so the assistant can perceive time passing between sessions'),
104
+
105
+ /** Strict allowlist of tool names to include. Only these tools will be available. Supports "*" glob matching. */
106
+ allowTools: z.array(z.string()).optional().describe('Strict allowlist of tool name patterns. Only matching tools are available. Supports * glob matching.'),
107
+
108
+ /** Denylist of tool names to exclude. Matching tools will be removed. Supports "*" glob matching. */
109
+ forbidTools: z.array(z.string()).optional().describe('Denylist of tool name patterns to exclude. Supports * glob matching.'),
110
+
111
+ /** Convenience alias for allowTools — an explicit list of tool names (exact matches only). */
112
+ toolNames: z.array(z.string()).optional().describe('Explicit list of tool names to include (exact match). Shorthand for allowTools without glob patterns.'),
113
+
114
+ /** Options passed through to the underlying OpenAI client (e.g. baseURL, apiKey). */
115
+ clientOptions: z.record(z.string(), z.any()).optional().describe('Options for the OpenAI client, passed through to the conversation'),
116
+ })
117
+
118
+ export type AssistantState = z.infer<typeof AssistantStateSchema>
119
+ export type AssistantOptions = z.infer<typeof AssistantOptionsSchema>
120
+
121
+ /** Fork options extended with assistant-specific tool filtering and lifecycle hooks. */
122
+ export type AssistantForkOptions = ForkOptions & {
123
+ /** Denylist of tool name patterns to exclude from the fork. Supports "*" glob matching. */
124
+ forbidTools?: string[]
125
+ /** Strict allowlist of tool name patterns for the fork. Supports "*" glob matching. */
126
+ allowTools?: string[]
127
+ /** Explicit list of tool names to include in the fork (exact match). */
128
+ toolNames?: string[]
129
+ /**
130
+ * Called with the forked assistant after it has been fully initialized (started, interceptors cloned,
131
+ * system prompt extensions copied, forkDepth set). Use this to add/remove tools, tweak state,
132
+ * inject system prompt extensions, or anything else before the fork is used.
133
+ */
134
+ onFork?: (fork: Assistant, parent: Assistant) => void | Promise<void>
135
+ }
136
+
137
+ export interface ResearchJobState {
138
+ status: 'running' | 'completed' | 'failed'
139
+ prompt: string
140
+ questions: string[]
141
+ results: (string | null)[]
142
+ errors: (string | null)[]
143
+ completed: number
144
+ total: number
145
+ }
146
+
147
+ export interface ResearchJobOptions {
148
+ prompt: string
149
+ questions: string[]
150
+ forkOptions: AssistantForkOptions
151
+ }
152
+
153
+ export type ResearchJobEvents = {
154
+ forkCompleted: [number, string]
155
+ forkError: [number, string]
156
+ completed: [string[]]
157
+ failed: [(string | null)[]]
158
+ }
159
+
160
+ export type ResearchJob = Entity<ResearchJobState, ResearchJobOptions, ResearchJobEvents>
161
+
162
+ /**
163
+ * An Assistant is a combination of a system prompt and tool calls that has a
164
+ * conversation with an LLM. You define an assistant by creating a folder with
165
+ * CORE.md (system prompt), tools.ts (tool implementations), and hooks.ts (event handlers).
166
+ *
167
+ * @extends Feature
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const assistant = container.feature('assistant', {
172
+ * folder: 'assistants/my-helper'
173
+ * })
174
+ * const answer = await assistant.ask('What capabilities do you have?')
175
+ * ```
176
+ */
177
+ export class Assistant extends Feature<AssistantState, AssistantOptions> {
178
+ static override stateSchema = AssistantStateSchema
179
+ static override optionsSchema = AssistantOptionsSchema
180
+ static override eventsSchema = AssistantEventsSchema
181
+ static override shortcut = 'features.assistant' as const
182
+
183
+ static { Feature.register(this, 'assistant') }
184
+
185
+ readonly interceptors = {
186
+ beforeAsk: new InterceptorChain<InterceptorPoints['beforeAsk']>(),
187
+ beforeTurn: new InterceptorChain<InterceptorPoints['beforeTurn']>(),
188
+ beforeToolCall: new InterceptorChain<InterceptorPoints['beforeToolCall']>(),
189
+ afterToolCall: new InterceptorChain<InterceptorPoints['afterToolCall']>(),
190
+ beforeResponse: new InterceptorChain<InterceptorPoints['beforeResponse']>(),
191
+ }
192
+
193
+ /**
194
+ * Extension point for plugins, setupToolsConsumer, and hooks to attach
195
+ * arbitrary methods to the assistant instance (e.g. voice-mode adding
196
+ * mute/unmute). Access via `assistant.ext.myMethod()`.
197
+ */
198
+ readonly ext: Record<string, (...args: any[]) => any> = {}
199
+
200
+ /**
201
+ * Observable runtime state that the assistant can manipulate freely via
202
+ * tool calls, hooks, or extensions. Unlike the feature's own `state`
203
+ * (which tracks internal lifecycle), mentalState is a blank slate for
204
+ * the assistant's own use — tracking mood, goals, context, preferences,
205
+ * or anything else. Fully observable so UI or other systems can react.
206
+ */
207
+ readonly mentalState = new State<Record<string, any>>()
208
+
209
+ /**
210
+ * Register an interceptor at a given point in the pipeline.
211
+ *
212
+ * @param point - The interception point
213
+ * @param fn - Middleware function receiving (ctx, next)
214
+ * @returns this, for chaining
215
+ */
216
+ intercept<K extends InterceptorPoint>(point: K, fn: InterceptorFn<InterceptorPoints[K]>): this {
217
+ if (!(point in this.interceptors)) {
218
+ const available = Object.keys(this.interceptors).join(', ')
219
+ throw new Error(`Unknown intercept point "${point}". Available points: ${available}`)
220
+ }
221
+ this.interceptors[point].add(fn as any)
222
+ return this
223
+ }
224
+
225
+ /**
226
+ * Trigger a named hook and await its completion. The hook function receives
227
+ * `(assistant, ...args)` and its return value is passed back to the caller.
228
+ * This ensures hooks run to completion BEFORE any subsequent logic executes,
229
+ * unlike the old bus-based approach where async hooks were fire-and-forget.
230
+ *
231
+ * Hooks that don't exist are silently skipped (returns undefined).
232
+ *
233
+ * @param hookName - The hook to trigger (matches an export name from hooks.ts)
234
+ * @param args - Arguments passed to the hook after the assistant instance
235
+ * @returns The hook's return value, or undefined if no hook exists
236
+ */
237
+ async triggerHook(hookName: string, ...args: any[]): Promise<any> {
238
+ const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
239
+ const hookFn = hooks[hookName]
240
+ if (!hookFn) return undefined
241
+ this.emit('hookFired', hookName)
242
+ return await hookFn(this, ...args)
243
+ }
244
+
245
+ /** @returns Default state with the assistant not started, zero conversations, and the resolved folder path. */
246
+ override get initialState(): AssistantState {
247
+ return {
248
+ ...super.initialState,
249
+ started: false,
250
+ conversationCount: 0,
251
+ lastResponse: '',
252
+ folder: this.resolvedFolder,
253
+ systemPrompt: '',
254
+ systemPromptExtensions: {},
255
+ meta: {},
256
+ tools: {},
257
+ hooks: {},
258
+ resumeThreadId: undefined,
259
+ pendingPlugins: [],
260
+ conversation: null,
261
+ subagents: {},
262
+ }
263
+ }
264
+
265
+
266
+ get name() {
267
+ return this.options.name || this.resolvedFolder.split('/').pop()
268
+ }
269
+
270
+ /** The absolute resolved path to the assistant folder. */
271
+ get resolvedFolder(): string {
272
+ return this.container.paths.resolve(this.options.folder)
273
+ }
274
+
275
+ /** The path to CORE.md which provides the system prompt. */
276
+ get corePromptPath(): string {
277
+ return this.paths.resolve('CORE.md')
278
+ }
279
+
280
+ /** The path to tools.ts which provides tool implementations and schemas. */
281
+ get toolsModulePath(): string {
282
+ return this.paths.resolve('tools.ts')
283
+ }
284
+
285
+ /** The path to hooks.ts which provides event handler functions. */
286
+ get hooksModulePath(): string {
287
+ return this.paths.resolve('hooks.ts')
288
+ }
289
+
290
+ /** Whether this assistant has a voice.yaml configuration file. */
291
+ get hasVoice(): boolean {
292
+ return this.container.fs.exists(this.paths.resolve('voice.yaml'))
293
+ }
294
+
295
+ /** Parsed voice configuration from voice.yaml, or undefined if not present. */
296
+ get voiceConfig(): Record<string, any> | undefined {
297
+ if (!this.hasVoice) return undefined
298
+ const yaml = this.container.feature('yaml')
299
+ return yaml.parse(String(this.container.fs.readFile(this.paths.resolve('voice.yaml'))))
300
+ }
301
+
302
+ get resolvedDocsFolder() {
303
+ const { docsFolder = this.effectiveOptions.docsFolder || 'docs' } = this.state.current
304
+
305
+ if (this.container.fs.exists(docsFolder)) {
306
+ return this.container.paths.resolve(docsFolder)
307
+ }
308
+
309
+ const findUp = this.container.fs.findUp('docs', {
310
+ cwd: this.resolvedFolder
311
+ })
312
+
313
+ if (typeof findUp === 'string' && this.container.fs.exists(findUp!)) {
314
+ this.state.set('docsFolder', findUp!)
315
+ return this.container.paths.resolve(findUp!)
316
+ }
317
+
318
+ return this.paths.resolve('docs')
319
+ }
320
+
321
+ /**
322
+ * Returns an instance of a ContentDb feature for the resolved docs folder
323
+ */
324
+ get contentDb() : ContentDb {
325
+ return this.container.feature('contentDb', { rootPath: this.resolvedDocsFolder })
326
+ }
327
+
328
+
329
+ /**
330
+ * Called immediately after the assistant is constructed. Synchronously loads
331
+ * the system prompt, tools, and hooks. Hooks are invoked via triggerHook()
332
+ * at each emit site, ensuring async hooks are properly awaited.
333
+ */
334
+ override afterInitialize() {
335
+ this.state.set('pendingPlugins', [])
336
+
337
+ // Load system prompt synchronously
338
+ this.state.set('systemPrompt', this.loadSystemPrompt())
339
+
340
+ // Load tools and hooks synchronously via vm.performSync
341
+ this.state.set('tools', this.loadTools())
342
+ this.state.set('hooks', this.loadHooks())
343
+
344
+ // Defer created hook+event so external listeners can register first
345
+ setTimeout(async () => {
346
+ await this.triggerHook('created')
347
+ this.emit('created')
348
+ }, 1)
349
+ }
350
+
351
+ get conversation(): Conversation {
352
+ let conv = this.state.get('conversation') as Conversation | null
353
+ if (!conv) {
354
+ conv = this.container.feature('conversation', {
355
+ model: this.effectiveOptions.model || 'gpt-5.4',
356
+ local: !!this.effectiveOptions.local,
357
+ tools: this.tools,
358
+ api: 'chat',
359
+ ...(this.effectiveOptions.maxTokens ? { maxTokens: this.effectiveOptions.maxTokens } : {}),
360
+ ...(this.effectiveOptions.temperature != null ? { temperature: this.effectiveOptions.temperature } : {}),
361
+ ...(this.effectiveOptions.topP != null ? { topP: this.effectiveOptions.topP } : {}),
362
+ ...(this.effectiveOptions.topK != null ? { topK: this.effectiveOptions.topK } : {}),
363
+ ...(this.effectiveOptions.frequencyPenalty != null ? { frequencyPenalty: this.effectiveOptions.frequencyPenalty } : {}),
364
+ ...(this.effectiveOptions.presencePenalty != null ? { presencePenalty: this.effectiveOptions.presencePenalty } : {}),
365
+ ...(this.effectiveOptions.stop ? { stop: this.effectiveOptions.stop } : {}),
366
+ ...(this.effectiveOptions.clientOptions ? { clientOptions: this.effectiveOptions.clientOptions } : {}),
367
+ history: [
368
+ { role: 'system', content: this.effectiveSystemPrompt },
369
+ ],
370
+ })
371
+ this.state.set('conversation', conv)
372
+ }
373
+ return conv
374
+ }
375
+
376
+ get availableTools() {
377
+ return Object.keys(this.tools)
378
+ }
379
+
380
+ get messages() {
381
+ return this.conversation.messages
382
+ }
383
+
384
+ /** Whether the assistant has been started and is ready to receive questions. */
385
+ get isStarted(): boolean {
386
+ return !!this.state.get('started')
387
+ }
388
+
389
+ /** Whether this assistant was created via fork(). */
390
+ get isFork(): boolean {
391
+ return (this.state.get('forkDepth') ?? 0) > 0
392
+ }
393
+
394
+ /** How many levels deep this fork is. 0 = original, 1 = direct fork, 2 = fork of a fork, etc. */
395
+ get forkDepth(): number {
396
+ return (this.state.get('forkDepth') as number) ?? 0
397
+ }
398
+
399
+ /** The current system prompt text. */
400
+ get systemPrompt(): string {
401
+ return this.state.get('systemPrompt') || ''
402
+ }
403
+
404
+ /** The named extensions appended to the system prompt. */
405
+ get systemPromptExtensions(): Record<string, string> {
406
+ return (this.state.get('systemPromptExtensions') || {}) as Record<string, string>
407
+ }
408
+
409
+ /** The system prompt with all extensions appended. This is the value passed to the conversation. */
410
+ get effectiveSystemPrompt(): string {
411
+ const base = this.systemPrompt
412
+ const extensions = Object.values(this.systemPromptExtensions)
413
+ if (!extensions.length) return base
414
+ return [base, ...extensions].join('\n\n')
415
+ }
416
+
417
+ /**
418
+ * Add or update a named system prompt extension. The value is appended
419
+ * to the base system prompt when passed to the conversation.
420
+ *
421
+ * @param key - A unique identifier for this extension
422
+ * @param value - The text to append
423
+ * @returns this, for chaining
424
+ */
425
+ addSystemPromptExtension(key: string, value: string): this {
426
+ this.state.set('systemPromptExtensions', { ...this.systemPromptExtensions, [key]: value })
427
+ this.syncSystemPromptToConversation()
428
+ this.emit('systemPromptExtensionsChanged')
429
+ return this
430
+ }
431
+
432
+ /**
433
+ * Remove a named system prompt extension.
434
+ *
435
+ * @param key - The identifier of the extension to remove
436
+ * @returns this, for chaining
437
+ */
438
+ removeSystemPromptExtension(key: string): this {
439
+ const current = { ...this.systemPromptExtensions }
440
+ delete current[key]
441
+ this.state.set('systemPromptExtensions', current)
442
+ this.syncSystemPromptToConversation()
443
+ this.emit('systemPromptExtensionsChanged')
444
+ return this
445
+ }
446
+
447
+ /** Update the conversation's system message to reflect the current effective prompt. */
448
+ private syncSystemPromptToConversation() {
449
+ const conv = this.state.get('conversation') as Conversation | null
450
+ if (!conv) return
451
+ const messages = [...conv.messages]
452
+ if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
453
+ messages[0] = { ...messages[0]!, content: this.effectiveSystemPrompt }
454
+ conv.state.set('messages', messages)
455
+ }
456
+ }
457
+
458
+ /** The tools registered with this assistant. */
459
+ get tools(): Record<string, ConversationTool> {
460
+ const all = (this.state.get('tools') || {}) as Record<string, ConversationTool>
461
+ return this.applyToolFilters(all)
462
+ }
463
+
464
+ /**
465
+ * Apply allowTools, forbidTools, and toolNames filters from options.
466
+ * toolNames is treated as an exact-match allowlist. allowTools/forbidTools support "*" glob patterns.
467
+ * allowTools is applied first (strict allowlist), then forbidTools removes from whatever remains.
468
+ */
469
+ private applyToolFilters(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
470
+ const { allowTools, forbidTools, toolNames } = this.effectiveOptions
471
+ if (!allowTools && !forbidTools && !toolNames) return tools
472
+
473
+ let names = Object.keys(tools)
474
+
475
+ // toolNames is a strict exact-match allowlist
476
+ if (toolNames) {
477
+ const allowed = new Set(toolNames)
478
+ names = names.filter(n => allowed.has(n))
479
+ }
480
+
481
+ // allowTools: only keep names matching at least one pattern
482
+ if (allowTools) {
483
+ names = names.filter(n => allowTools.some(pattern => this.matchToolPattern(pattern, n)))
484
+ }
485
+
486
+ // forbidTools: remove names matching any pattern
487
+ if (forbidTools) {
488
+ names = names.filter(n => !forbidTools.some(pattern => this.matchToolPattern(pattern, n)))
489
+ }
490
+
491
+ const result: Record<string, ConversationTool> = {}
492
+ for (const n of names) {
493
+ const tool = tools[n]
494
+ if (tool) result[n] = tool
495
+ }
496
+ return result
497
+ }
498
+
499
+ /**
500
+ * Match a tool name against a pattern that supports "*" as a wildcard.
501
+ * - "*" matches everything
502
+ * - "prefix*" matches names starting with prefix
503
+ * - "*suffix" matches names ending with suffix
504
+ * - "pre*suf" matches names starting with pre and ending with suf
505
+ * - exact string matches exactly
506
+ */
507
+ private matchToolPattern(pattern: string, name: string): boolean {
508
+ if (pattern === '*') return true
509
+ if (!pattern.includes('*')) return pattern === name
510
+
511
+ // Convert glob pattern to regex: escape regex chars, replace * with .*
512
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
513
+ return new RegExp(`^${escaped}$`).test(name)
514
+ }
515
+
516
+ /**
517
+ * Apply a setup function or a Helper instance to this assistant.
518
+ *
519
+ * When passed a function, it receives the assistant and can configure
520
+ * tools, hooks, event listeners, etc.
521
+ *
522
+ * When passed a Helper instance that exposes tools via toTools(),
523
+ * those tools are automatically added to this assistant.
524
+ *
525
+ * @param fnOrHelper - Setup function or Helper instance
526
+ * @returns this, for chaining
527
+ *
528
+ * @example
529
+ * ```typescript
530
+ * assistant
531
+ * .use(setupLogging)
532
+ * .use(container.feature('git'))
533
+ * ```
534
+ */
535
+ use(fnOrHelper: ((assistant: this) => void | Promise<void>) | { toTools: () => { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> } } | { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }): this {
536
+ if (typeof fnOrHelper === 'function') {
537
+ const result = fnOrHelper(this)
538
+ if (result && typeof (result as any).then === 'function') {
539
+ const pending = this.state.get('pendingPlugins') as Promise<void>[]
540
+ this.state.set('pendingPlugins', [...pending, result as Promise<void>])
541
+ }
542
+ } else if (fnOrHelper && typeof (fnOrHelper as any).toTools === 'function') {
543
+ this._registerTools((fnOrHelper as any).toTools())
544
+ if (typeof (fnOrHelper as any).setupToolsConsumer === 'function') {
545
+ (fnOrHelper as any).setupToolsConsumer(this)
546
+ }
547
+ } else if (fnOrHelper && 'schemas' in fnOrHelper && 'handlers' in fnOrHelper) {
548
+ this._registerTools(fnOrHelper as { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> })
549
+ if (typeof (fnOrHelper as any).setup === 'function') {
550
+ (fnOrHelper as any).setup(this)
551
+ }
552
+ }
553
+ return this
554
+ }
555
+
556
+ /** Register tools from a `{ schemas, handlers }` object. */
557
+ private _registerTools({ schemas, handlers }: { schemas: Record<string, z.ZodType>, handlers: Record<string, Function> }) {
558
+ for (const name of Object.keys(schemas)) {
559
+ if (typeof handlers[name] === 'function') {
560
+ this.addTool(name, handlers[name] as any, schemas[name])
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Add a tool to this assistant. The tool name is derived from the
567
+ * handler's function name.
568
+ *
569
+ * @param handler - A named function that implements the tool
570
+ * @param schema - Optional Zod schema describing the tool's parameters
571
+ * @returns this, for chaining
572
+ *
573
+ * @example
574
+ * ```typescript
575
+ * assistant.addTool(function getWeather(args) {
576
+ * return { temp: 72 }
577
+ * }, z.object({ city: z.string() }).describe('Get weather for a city'))
578
+ * ```
579
+ */
580
+ addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this
581
+ addTool(handler: (...args: any[]) => any, schema?: z.ZodType): this
582
+ addTool(nameOrHandler: string | ((...args: any[]) => any), handlerOrSchema?: ((...args: any[]) => any) | z.ZodType, maybeSchema?: z.ZodType): this {
583
+ let name: string
584
+ let handler: (...args: any[]) => any
585
+ let schema: z.ZodType | undefined
586
+
587
+ if (typeof nameOrHandler === 'function') {
588
+ // addTool(handler, schema?) — extract name from function
589
+ handler = nameOrHandler
590
+ name = handler.name
591
+ schema = handlerOrSchema as z.ZodType | undefined
592
+ } else {
593
+ // addTool(name, handler, schema?)
594
+ name = nameOrHandler
595
+ handler = handlerOrSchema as (...args: any[]) => any
596
+ schema = maybeSchema
597
+ }
598
+
599
+ if (!name) throw new Error('addTool handler must be a named function')
600
+ if (!this._runtimeToolNames) this._runtimeToolNames = new Set()
601
+ this._runtimeToolNames.add(name)
602
+
603
+ const current = { ...this.tools }
604
+
605
+ if (schema) {
606
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
607
+ // OpenAI requires `required` to list ALL property keys — optional params
608
+ // must still appear in `required` but use a default value in the schema.
609
+ const properties = jsonSchema.properties || {}
610
+ const required = Object.keys(properties)
611
+ current[name] = {
612
+ handler: handler as ConversationTool['handler'],
613
+ description: jsonSchema.description || name,
614
+ parameters: {
615
+ type: jsonSchema.type || 'object',
616
+ properties,
617
+ required,
618
+ },
619
+ }
620
+ } else {
621
+ current[name] = {
622
+ handler: handler as ConversationTool['handler'],
623
+ description: name,
624
+ parameters: { type: 'object', properties: {} },
625
+ }
626
+ }
627
+
628
+ this.state.set('tools', current)
629
+ this.emit('toolsChanged')
630
+
631
+ return this
632
+ }
633
+
634
+ /**
635
+ * Remove a tool by name or handler function reference.
636
+ *
637
+ * @param nameOrHandler - The tool name string, or the handler function to match
638
+ * @returns this, for chaining
639
+ */
640
+ removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
641
+ const current = { ...this.tools }
642
+
643
+ if (typeof nameOrHandler === 'string') {
644
+ delete current[nameOrHandler]
645
+ this._runtimeToolNames?.delete(nameOrHandler)
646
+ } else {
647
+ for (const [name, tool] of Object.entries(current)) {
648
+ if (tool.handler === nameOrHandler) {
649
+ delete current[name]
650
+ this._runtimeToolNames?.delete(name)
651
+ break
652
+ }
653
+ }
654
+ }
655
+
656
+ this.state.set('tools', current)
657
+ this.emit('toolsChanged')
658
+
659
+ return this
660
+ }
661
+
662
+ /**
663
+ * Simulate a tool call and its result by appending the appropriate
664
+ * messages to the conversation history. Useful for injecting context
665
+ * that looks like the assistant performed a tool call.
666
+ *
667
+ * @param toolCallName - The name of the tool
668
+ * @param args - The arguments that were "passed" to the tool
669
+ * @param result - The result the tool "returned"
670
+ * @returns this, for chaining
671
+ */
672
+ simulateToolCallWithResult(toolCallName: string, args: Record<string, any>, result: any): this {
673
+ if (!this.conversation) {
674
+ throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
675
+ }
676
+
677
+ const callId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
678
+
679
+ this.conversation.pushMessage({
680
+ role: 'assistant',
681
+ content: null,
682
+ tool_calls: [{
683
+ id: callId,
684
+ type: 'function',
685
+ function: {
686
+ name: toolCallName,
687
+ arguments: JSON.stringify(args),
688
+ },
689
+ }],
690
+ } as Message)
691
+
692
+ this.conversation.pushMessage({
693
+ role: 'tool',
694
+ tool_call_id: callId,
695
+ content: typeof result === 'string' ? result : JSON.stringify(result),
696
+ } as Message)
697
+
698
+ return this
699
+ }
700
+
701
+ /**
702
+ * Simulate a user question and assistant response by appending both
703
+ * messages to the conversation history.
704
+ *
705
+ * @param question - The user's question
706
+ * @param response - The assistant's response
707
+ * @returns this, for chaining
708
+ */
709
+ simulateQuestionAndResponse(question: string, response: string): this {
710
+ if (!this.conversation) {
711
+ throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
712
+ }
713
+
714
+ this.conversation.pushMessage({ role: 'user', content: question })
715
+ this.conversation.pushMessage({ role: 'assistant', content: response })
716
+
717
+ return this
718
+ }
719
+
720
+ /**
721
+ * Parsed YAML frontmatter from CORE.md, or empty object if none.
722
+ */
723
+ get meta(): Record<string, any> {
724
+ return (this.state.get('meta') || {}) as Record<string, any>
725
+ }
726
+
727
+ /**
728
+ * Merged options where CORE.md frontmatter provides defaults and
729
+ * constructor options take precedence. Prefer this over `this.options`
730
+ * anywhere model parameters or runtime config is consumed.
731
+ */
732
+ get effectiveOptions(): AssistantOptions & Record<string, any> {
733
+ return { ...this.meta, ...this.options }
734
+ }
735
+
736
+ /**
737
+ * Load the system prompt from CORE.md, applying any prepend/append options.
738
+ * YAML frontmatter (between --- fences) is stripped from the prompt and
739
+ * stored in `_meta`.
740
+ *
741
+ * @returns {string} The assembled system prompt
742
+ */
743
+ loadSystemPrompt(): string {
744
+ const { fs } = this.container
745
+ let prompt = ''
746
+ this.state.set('meta', {})
747
+
748
+ if (this.options.systemPrompt) {
749
+ prompt = this.options.systemPrompt
750
+ } else if (fs.exists(this.corePromptPath)) {
751
+ const raw = fs.readFile(this.corePromptPath).toString()
752
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
753
+
754
+ if (fmMatch) {
755
+ const yaml = this.container.feature('yaml')
756
+ this.state.set('meta', yaml.parse(fmMatch[1]!) ?? {})
757
+ prompt = raw.slice(fmMatch[0].length)
758
+ } else {
759
+ prompt = raw
760
+ }
761
+ }
762
+
763
+ if (this.options.prependPrompt) {
764
+ prompt = this.options.prependPrompt + '\n\n' + prompt
765
+ }
766
+
767
+ if (this.options.appendPrompt) {
768
+ prompt = prompt + '\n\n' + this.options.appendPrompt
769
+ }
770
+
771
+ if (this.options.injectTimestamps) {
772
+ prompt = prompt + '\n\n' + [
773
+ '## Timestamps',
774
+ 'Each user message is prefixed with a timestamp in [YYYY-MM-DD HH:MM] format.',
775
+ 'Use these to understand the passage of time between interactions.',
776
+ 'The user may return hours or days later within the same conversation — acknowledge the time gap naturally when relevant, and use timestamps to contextualize when topics were previously discussed.',
777
+ ].join('\n')
778
+ }
779
+
780
+ return prompt.trim()
781
+ }
782
+
783
+ /**
784
+ * Load tools from tools.ts using the container's VM feature, injecting
785
+ * the container and assistant as globals. Merges with any tools
786
+ * provided in the constructor options. Runs synchronously via vm.loadModule.
787
+ *
788
+ * @returns {Record<string, ConversationTool>} The assembled tool map
789
+ */
790
+ loadTools(): Record<string, ConversationTool> {
791
+ const tools: Record<string, ConversationTool> = {}
792
+
793
+ // Skip loading if no tools file exists (runtime-created assistants)
794
+ if (!this.container.fs.exists(this.toolsModulePath)) {
795
+ return this.mergeOptionTools(tools)
796
+ }
797
+
798
+ // Ensure virtual modules (zod, luca, etc.) are seeded so tools
799
+ // files outside the project tree can resolve them through the VM
800
+ if (this.container.features.has('helpers')) {
801
+ const helpers = this.container.feature('helpers') as any
802
+ helpers.seedVirtualModules()
803
+ }
804
+
805
+ const vm = this.container.feature('vm')
806
+
807
+ let moduleExports: Record<string, any>
808
+ try {
809
+ moduleExports = vm.loadModule(this.toolsModulePath, {
810
+ container: this.container,
811
+ me: this,
812
+ my: this,
813
+ assistant: this,
814
+ console: console,
815
+ })
816
+ } catch (err: any) {
817
+ console.error(`Failed to load tools from ${this.toolsModulePath}`)
818
+ console.error(`There may be a syntax error in this file. Please check it.`)
819
+ console.error(err.message || err)
820
+ return this.mergeOptionTools(tools)
821
+ }
822
+
823
+ // Stash `export const use = [...]` for deferred processing during start(),
824
+ // since the assistant isn't fully constructed yet when loadTools() runs
825
+ if (Array.isArray(moduleExports.use)) {
826
+ this.state.set('deferredUse', moduleExports.use)
827
+ }
828
+
829
+ if (Object.keys(moduleExports).length) {
830
+ const schemas: Record<string, z.ZodType> = moduleExports.schemas || {}
831
+
832
+ for (const [name, fn] of Object.entries(moduleExports)) {
833
+ if (name === 'schemas' || name === 'default' || name === 'use' || typeof fn !== 'function') continue
834
+
835
+ const schema = schemas[name]
836
+ if (schema) {
837
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
838
+ tools[name] = {
839
+ handler: fn as ConversationTool['handler'],
840
+ description: jsonSchema.description || name,
841
+ parameters: {
842
+ type: jsonSchema.type || 'object',
843
+ properties: jsonSchema.properties || {},
844
+ ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
845
+ },
846
+ }
847
+ } else {
848
+ tools[name] = {
849
+ handler: fn as ConversationTool['handler'],
850
+ description: name,
851
+ parameters: { type: 'object', properties: {} },
852
+ }
853
+ }
854
+ }
855
+ }
856
+
857
+ return this.mergeOptionTools(tools)
858
+ }
859
+
860
+ /**
861
+ * Merge tools provided via constructor options into the tool map.
862
+ * This allows runtime-created assistants to define tools entirely via options.
863
+ */
864
+ private mergeOptionTools(tools: Record<string, ConversationTool>): Record<string, ConversationTool> {
865
+ if (this.options.tools) {
866
+ const optionSchemas = this.options.schemas || {}
867
+
868
+ for (const [name, fn] of Object.entries(this.options.tools)) {
869
+ if (typeof fn !== 'function') continue
870
+
871
+ const schema = optionSchemas[name]
872
+ if (schema) {
873
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
874
+ tools[name] = {
875
+ handler: fn as ConversationTool['handler'],
876
+ description: jsonSchema.description || name,
877
+ parameters: {
878
+ type: jsonSchema.type || 'object',
879
+ properties: jsonSchema.properties || {},
880
+ ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
881
+ },
882
+ }
883
+ } else {
884
+ tools[name] = {
885
+ handler: fn as ConversationTool['handler'],
886
+ description: name,
887
+ parameters: { type: 'object', properties: {} },
888
+ }
889
+ }
890
+ }
891
+ }
892
+
893
+ return tools
894
+ }
895
+
896
+ /**
897
+ * Load event hooks from hooks.ts. Each exported function name should
898
+ * match an event the assistant emits. When that event fires, the
899
+ * corresponding hook function is called. Runs synchronously via vm.loadModule.
900
+ *
901
+ * @returns {Record<string, Function>} The hook function map
902
+ */
903
+ loadHooks(): Record<string, (...args: any[]) => any> {
904
+ const hooks: Record<string, (...args: any[]) => any> = {}
905
+
906
+ // Skip loading if no hooks file exists (runtime-created assistants)
907
+ if (!this.container.fs.exists(this.hooksModulePath)) {
908
+ return hooks
909
+ }
910
+
911
+ const vm = this.container.feature('vm')
912
+
913
+ let moduleExports: Record<string, any>
914
+ try {
915
+ moduleExports = vm.loadModule(this.hooksModulePath, {
916
+ container: this.container,
917
+ me: this,
918
+ my: this,
919
+ assistant: this,
920
+ console: console,
921
+ })
922
+ } catch (err: any) {
923
+ console.error(`Failed to load hooks from ${this.hooksModulePath}`)
924
+ console.error(`There may be a syntax error in this file. Please check it.`)
925
+ console.error(err.message || err)
926
+ return hooks
927
+ }
928
+
929
+ for (const [name, fn] of Object.entries(moduleExports)) {
930
+ if (name === 'default' || typeof fn !== 'function') continue
931
+ hooks[name] = fn as (...args: any[]) => any
932
+ }
933
+
934
+ return hooks
935
+ }
936
+
937
+ /**
938
+ * Provides a helper for creating paths off of the assistant's base folder
939
+ */
940
+ get paths() {
941
+ const { container } = this
942
+ const base = this.resolvedFolder
943
+
944
+ return {
945
+ resolve(...args: any[]) {
946
+ return container.paths.resolve(base, ...args)
947
+ },
948
+ join(...args: any[]) {
949
+ return container.paths.resolve(base, ...args)
950
+ }
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Prepend a [YYYY-MM-DD HH:MM] timestamp to user message content.
956
+ */
957
+ private prependTimestamp(content: string | ContentPart[]): string | ContentPart[] {
958
+ const now = new Date()
959
+ const pad = (n: number) => String(n).padStart(2, '0')
960
+ const stamp = `[${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}]`
961
+
962
+ if (typeof content === 'string') {
963
+ return `${stamp} ${content}`
964
+ }
965
+
966
+ const firstPart = content[0]
967
+ if (firstPart && firstPart.type === 'text') {
968
+ return [{ type: 'text' as const, text: `${stamp} ${firstPart.text}` }, ...content.slice(1)]
969
+ }
970
+
971
+ return [{ type: 'text' as const, text: stamp }, ...content]
972
+ }
973
+
974
+ // -- History mode helpers --
975
+
976
+ /** The assistant name derived from the folder basename. */
977
+ get assistantName(): string {
978
+ return this.resolvedFolder.split('/').pop() || 'assistant'
979
+ }
980
+
981
+ /** An 8-char hash of the container cwd for per-project thread isolation. */
982
+ get cwdHash(): string {
983
+ return hashObject(this.container.cwd).slice(0, 8)
984
+ }
985
+
986
+ /** The thread prefix for this assistant+project combination. */
987
+ get threadPrefix(): string {
988
+ return `${this.assistantName}:${this.cwdHash}:`
989
+ }
990
+
991
+ /** Build a thread ID based on the history mode. */
992
+ private buildThreadId(mode: string): string {
993
+ const prefix = this.threadPrefix
994
+ switch (mode) {
995
+ case 'daily': {
996
+ const today = new Date().toISOString().slice(0, 10)
997
+ return `${prefix}${today}`
998
+ }
999
+ case 'persistent':
1000
+ return `${prefix}persistent`
1001
+ case 'session':
1002
+ return `${prefix}${this.uuid}`
1003
+ default:
1004
+ return `${prefix}${this.uuid}`
1005
+ }
1006
+ }
1007
+
1008
+ /** The conversationHistory feature instance. */
1009
+ get conversationHistory(): ConversationHistory {
1010
+ return this.container.feature('conversationHistory') as ConversationHistory
1011
+ }
1012
+
1013
+ /** The active thread ID (undefined in lifecycle mode). */
1014
+ get currentThreadId(): string | undefined {
1015
+ return this.state.get('threadId')
1016
+ }
1017
+
1018
+ /**
1019
+ * Override thread for resume. Call before start().
1020
+ *
1021
+ * @param threadId - The thread ID to resume
1022
+ * @returns this, for chaining
1023
+ */
1024
+ resumeThread(threadId: string): this {
1025
+ this.state.set('resumeThreadId', threadId)
1026
+ return this
1027
+ }
1028
+
1029
+ /**
1030
+ * List saved conversations for this assistant+project.
1031
+ *
1032
+ * @param opts - Optional limit
1033
+ * @returns Conversation metadata records
1034
+ */
1035
+ async listHistory(opts?: { limit?: number }): Promise<ConversationMeta[]> {
1036
+ const metas = await this.conversationHistory.findByThreadPrefix(this.threadPrefix)
1037
+ if (opts?.limit) return metas.slice(0, opts.limit)
1038
+ return metas
1039
+ }
1040
+
1041
+ /**
1042
+ * Delete all history for this assistant+project.
1043
+ *
1044
+ * @returns Number of conversations deleted
1045
+ */
1046
+ async clearHistory(): Promise<number> {
1047
+ return this.conversationHistory.deleteByThreadPrefix(this.threadPrefix)
1048
+ }
1049
+
1050
+ /**
1051
+ * Load history into the conversation after it's been created.
1052
+ * Called from start() for non-lifecycle modes.
1053
+ */
1054
+ private async loadConversationHistory(): Promise<void> {
1055
+ const mode = this.effectiveOptions.historyMode || 'lifecycle'
1056
+ if (mode === 'lifecycle') return
1057
+
1058
+ const threadId = (this.state.get('resumeThreadId') as string | undefined) || this.buildThreadId(mode)
1059
+ this.state.set('threadId', threadId)
1060
+
1061
+ const existing = await this.conversationHistory.findByThread(threadId)
1062
+
1063
+ if (existing) {
1064
+ // Replace conversation messages with loaded history
1065
+ const messages = [...existing.messages]
1066
+
1067
+ // Swap in fresh system prompt if it changed
1068
+ if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
1069
+ messages[0] = { role: messages[0]!.role, content: this.effectiveSystemPrompt }
1070
+ }
1071
+
1072
+ this.conversation.state.set('id', existing.id)
1073
+ this.conversation.state.set('thread', threadId)
1074
+ this.conversation.state.set('messages', messages)
1075
+ this.state.set('conversationId', existing.id)
1076
+
1077
+ // Restore lastResponseId so the Responses API can continue the chain
1078
+ if (existing.metadata?.lastResponseId) {
1079
+ this.conversation.state.set('lastResponseId', existing.metadata.lastResponseId)
1080
+ }
1081
+ } else {
1082
+ // Fresh conversation — just set thread
1083
+ this.conversation.state.set('thread', threadId)
1084
+ this.state.set('conversationId', this.conversation.state.get('id'))
1085
+ }
1086
+ }
1087
+
1088
+ /** Tool names added at runtime via addTool()/use(), so reload() can preserve them. */
1089
+ private _runtimeToolNames!: Set<string>
1090
+
1091
+ /**
1092
+ * Reload tools, hooks, and system prompt from disk. Useful during development
1093
+ * or when tool/hook files have been modified and you want the assistant to
1094
+ * pick up changes without restarting.
1095
+ *
1096
+ * @returns this, for chaining
1097
+ */
1098
+ reload(): this {
1099
+ // Snapshot runtime-added tools before reloading from disk
1100
+ const runtimeTools: Record<string, ConversationTool> = {}
1101
+ if (this._runtimeToolNames?.size) {
1102
+ const current = this.tools
1103
+ for (const name of this._runtimeToolNames) {
1104
+ if (current[name]) runtimeTools[name] = current[name]
1105
+ }
1106
+ }
1107
+
1108
+ // Reload system prompt from disk
1109
+ this.state.set('systemPrompt', this.loadSystemPrompt())
1110
+
1111
+ // Reload tools from disk (merges with option tools), then restore runtime tools
1112
+ const diskTools = this.loadTools()
1113
+ this.state.set('tools', { ...diskTools, ...runtimeTools })
1114
+
1115
+ // Re-process deferred `use` entries (export const use = [...] in tools.ts).
1116
+ // These replace tools from the same features, which is a no-op when unchanged.
1117
+ const deferredUse = this.state.get('deferredUse') as any[] | undefined
1118
+ if (deferredUse?.length) {
1119
+ for (const entry of deferredUse) {
1120
+ this.use(entry)
1121
+ }
1122
+ this.state.set('deferredUse', undefined)
1123
+ }
1124
+
1125
+ this.emit('toolsChanged')
1126
+
1127
+ // Reload hooks from disk — triggerHook reads from state so new hooks are active immediately
1128
+ this.state.set('hooks', this.loadHooks())
1129
+
1130
+ this.emit('reloaded')
1131
+
1132
+ return this
1133
+ }
1134
+
1135
+ /**
1136
+ * Start the assistant by creating the conversation and wiring up events.
1137
+ * The system prompt, tools, and hooks are already loaded synchronously
1138
+ * during initialization.
1139
+ *
1140
+ * @returns {Promise<this>} The initialized assistant
1141
+ */
1142
+ async start(): Promise<this> {
1143
+ // Prevent duplicate listener registration if already started
1144
+ if (this.isStarted) return this
1145
+
1146
+ // Process deferred `use` entries from tools.ts (stashed during loadTools
1147
+ // because the assistant isn't fully constructed at that point)
1148
+ const deferredUse = this.state.get('deferredUse') as any[] | undefined
1149
+ if (deferredUse?.length) {
1150
+ for (const entry of deferredUse) {
1151
+ this.use(entry)
1152
+ }
1153
+ this.state.set('deferredUse', undefined)
1154
+ }
1155
+
1156
+ // Allow hooks to run before the assistant starts (blocks until complete)
1157
+ await this.triggerHook('beforeStart')
1158
+
1159
+ // Wait for any async .use() plugins to finish before starting
1160
+ const pending = this.state.get('pendingPlugins') as Promise<void>[]
1161
+ if (pending.length) {
1162
+ await Promise.all(pending)
1163
+ this.state.set('pendingPlugins', [])
1164
+ }
1165
+
1166
+ // Allow hooks.ts to export a formatSystemPrompt(assistant, prompt) => string
1167
+ // that transforms the system prompt before the conversation is created.
1168
+ const formatted = await this.triggerHook('formatSystemPrompt', this.systemPrompt)
1169
+ if (typeof formatted === 'string') {
1170
+ this.state.set('systemPrompt', formatted)
1171
+ }
1172
+
1173
+ // Wire up event forwarding from conversation to assistant.
1174
+ // Each forwarded event triggers its hook (awaited) before emitting on the assistant bus.
1175
+ this.conversation.on('turnStart', async (info: any) => {
1176
+ await this.triggerHook('turnStart', info)
1177
+ this.emit('turnStart', info)
1178
+ })
1179
+ this.conversation.on('turnEnd', async (info: any) => {
1180
+ await this.triggerHook('turnEnd', info)
1181
+ this.emit('turnEnd', info)
1182
+ })
1183
+ this.conversation.on('chunk', async (chunk: string) => {
1184
+ await this.triggerHook('chunk', chunk)
1185
+ this.emit('chunk', chunk)
1186
+ })
1187
+ this.conversation.on('preview', async (text: string) => {
1188
+ await this.triggerHook('preview', text)
1189
+ this.emit('preview', text)
1190
+ })
1191
+ this.conversation.on('response', async (text: string) => {
1192
+ await this.triggerHook('response', text)
1193
+ this.emit('response', text)
1194
+ this.state.set('lastResponse', text)
1195
+ })
1196
+ this.conversation.on('rawEvent', async (event: any) => {
1197
+ await this.triggerHook('rawEvent', event)
1198
+ this.emit('rawEvent', event)
1199
+ })
1200
+ this.conversation.on('mcpEvent', async (event: any) => {
1201
+ await this.triggerHook('mcpEvent', event)
1202
+ this.emit('mcpEvent', event)
1203
+ })
1204
+ this.conversation.on('toolCall', async (name: string, args: any) => {
1205
+ await this.triggerHook('toolCall', name, args)
1206
+ this.emit('toolCall', name, args)
1207
+ })
1208
+ this.conversation.on('toolResult', async (name: string, result: any) => {
1209
+ await this.triggerHook('toolResult', name, result)
1210
+ this.emit('toolResult', name, result)
1211
+ })
1212
+ this.conversation.on('toolError', async (name: string, error: any) => {
1213
+ await this.triggerHook('toolError', name, error)
1214
+ this.emit('toolError', name, error)
1215
+ })
1216
+
1217
+ // Install interceptor-aware tool executor on the conversation
1218
+ this.conversation.toolExecutor = async (name: string, args: Record<string, any>, handler: (...a: any[]) => Promise<any>) => {
1219
+ const ctx = { name, args, result: undefined as string | undefined, error: undefined, skip: false }
1220
+
1221
+ // Hook runs first (awaited), then interceptor chain
1222
+ await this.triggerHook('beforeToolCall', ctx)
1223
+ await this.interceptors.beforeToolCall.run(ctx, async () => {})
1224
+
1225
+ if (ctx.skip) {
1226
+ const result = ctx.result ?? JSON.stringify({ skipped: true })
1227
+ await this.triggerHook('toolResult', ctx.name, result)
1228
+ this.emit('toolResult', ctx.name, result)
1229
+ return result
1230
+ }
1231
+
1232
+ try {
1233
+ await this.triggerHook('toolCall', ctx.name, ctx.args)
1234
+ this.emit('toolCall', ctx.name, ctx.args)
1235
+ const output = await handler(ctx.args)
1236
+ ctx.result = typeof output === 'string' ? output : JSON.stringify(output)
1237
+ } catch (err: any) {
1238
+ ctx.error = err
1239
+ ctx.result = JSON.stringify({ error: err.message || String(err) })
1240
+ }
1241
+
1242
+ // Hook runs first (awaited), then interceptor chain
1243
+ await this.triggerHook('afterToolCall', ctx)
1244
+ await this.interceptors.afterToolCall.run(ctx, async () => {})
1245
+
1246
+ if (ctx.error && !ctx.result?.includes('"error"')) {
1247
+ await this.triggerHook('toolError', ctx.name, ctx.error)
1248
+ this.emit('toolError', ctx.name, ctx.error)
1249
+ } else {
1250
+ await this.triggerHook('toolResult', ctx.name, ctx.result!)
1251
+ this.emit('toolResult', ctx.name, ctx.result!)
1252
+ }
1253
+
1254
+ return ctx.result!
1255
+ }
1256
+
1257
+ // Load conversation history for non-lifecycle modes
1258
+ await this.loadConversationHistory()
1259
+
1260
+ // Enable autoCompact for modes that accumulate history
1261
+ const mode = this.effectiveOptions.historyMode || 'lifecycle'
1262
+ if (mode === 'daily' || mode === 'persistent') {
1263
+ (this.conversation.options as any).autoCompact = true
1264
+ }
1265
+
1266
+ this.on('toolsChanged', () => {
1267
+ const conv = this.state.get('conversation') as Conversation | null
1268
+ if (conv) {
1269
+ conv.updateTools(this.tools)
1270
+ }
1271
+ })
1272
+
1273
+ this.state.set('started', true)
1274
+ await this.triggerHook('started')
1275
+ this.emit('started')
1276
+
1277
+ // afterStart blocks until complete — use for setup that needs the full assistant ready
1278
+ await this.triggerHook('afterStart')
1279
+
1280
+ return this
1281
+ }
1282
+
1283
+ /**
1284
+ * Ask the assistant a question. It will use its tools to produce
1285
+ * a streamed response. The assistant auto-starts if needed.
1286
+ *
1287
+ * @param {string | ContentPart[]} question - The question to ask
1288
+ * @returns {Promise<string>} The assistant's response
1289
+ *
1290
+ * @example
1291
+ * ```typescript
1292
+ * const answer = await assistant.ask('What capabilities do you have?')
1293
+ * ```
1294
+ */
1295
+ async ask(question: string | ContentPart[], options?: AskOptions): Promise<string> {
1296
+ if (!this.isStarted) {
1297
+ await this.start()
1298
+ }
1299
+
1300
+ if (!this.conversation) {
1301
+ return 'Assistant is not started'
1302
+ }
1303
+
1304
+ const count = (this.state.get('conversationCount') || 0) + 1
1305
+ this.state.set('conversationCount', count)
1306
+
1307
+ if (this.effectiveOptions.injectTimestamps) {
1308
+ question = this.prependTimestamp(question)
1309
+ }
1310
+
1311
+ // Trigger beforeInitialAsk only on the first ask() call
1312
+ if (count === 1) {
1313
+ await this.triggerHook('beforeInitialAsk', question, options)
1314
+ }
1315
+
1316
+ // Trigger beforeAsk hook on every ask() call — can modify question via return value
1317
+ const hookResult = await this.triggerHook('beforeAsk', question, options)
1318
+ if (typeof hookResult === 'string') {
1319
+ question = hookResult
1320
+ }
1321
+
1322
+ // Run beforeAsk interceptors — they can rewrite the question or short-circuit
1323
+ if (this.interceptors.beforeAsk.hasInterceptors) {
1324
+ const ctx = { question, options } as InterceptorPoints['beforeAsk']
1325
+ await this.interceptors.beforeAsk.run(ctx, async () => {})
1326
+ if (ctx.result !== undefined) return ctx.result
1327
+ question = ctx.question
1328
+ options = ctx.options
1329
+ }
1330
+
1331
+ let result = await this.conversation.ask(question, options)
1332
+
1333
+ // Run beforeResponse interceptors — they can rewrite the final text
1334
+ if (this.interceptors.beforeResponse.hasInterceptors) {
1335
+ const ctx = { text: result }
1336
+ await this.interceptors.beforeResponse.run(ctx, async () => {})
1337
+ result = ctx.text
1338
+ }
1339
+
1340
+ // Auto-save for non-lifecycle modes
1341
+ if (this.effectiveOptions.historyMode !== 'lifecycle' && this.state.get('threadId')) {
1342
+ await this.conversation.save({ thread: this.state.get('threadId') })
1343
+ }
1344
+
1345
+ await this.triggerHook('answered', result)
1346
+ this.emit('answered', result)
1347
+
1348
+ return result
1349
+ }
1350
+
1351
+ /**
1352
+ * Save the conversation to disk via conversationHistory.
1353
+ *
1354
+ * @param opts - Optional overrides for title, tags, thread, or metadata
1355
+ * @returns The saved conversation record
1356
+ */
1357
+ async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
1358
+ if (!this.conversation) {
1359
+ throw new Error('Cannot save: assistant has no active conversation')
1360
+ }
1361
+
1362
+ return this.conversation.save(opts)
1363
+ }
1364
+
1365
+ // -- Fork & Research API --
1366
+
1367
+ /**
1368
+ * Fork the assistant into a new independent instance. The fork gets its own
1369
+ * conversation (with configurable history truncation) but preserves the
1370
+ * assistant's full identity: interceptors, tools, hooks, system prompt extensions.
1371
+ *
1372
+ * @param options - Fork options including history truncation and conversation overrides
1373
+ * - `history: 'full'` (default) — deep copy all messages
1374
+ * - `history: 'none'` — system prompt only
1375
+ * - `history: number` — keep last N exchanges + system prompt
1376
+ * - Plus any conversation creation overrides (model, maxTokens, temperature, etc.)
1377
+ *
1378
+ * When called with an array, creates multiple independent forks.
1379
+ *
1380
+ * @example
1381
+ * ```typescript
1382
+ * // Single fork with no history, cheap model
1383
+ * const fork = await assistant.fork({ history: 'none', model: 'gpt-4o-mini' })
1384
+ * const answer = await fork.ask('Quick factual question')
1385
+ *
1386
+ * // Multiple forks
1387
+ * const [a, b] = await assistant.fork([
1388
+ * { history: 'none' },
1389
+ * { history: 3 },
1390
+ * ])
1391
+ * ```
1392
+ */
1393
+ async fork(options?: AssistantForkOptions): Promise<Assistant>
1394
+ async fork(options?: AssistantForkOptions[]): Promise<Assistant[]>
1395
+ async fork(options: AssistantForkOptions | AssistantForkOptions[] = {}): Promise<Assistant | Assistant[]> {
1396
+ if (Array.isArray(options)) {
1397
+ return Promise.all(options.map(o => this.fork(o)))
1398
+ }
1399
+
1400
+ if (!this.isStarted) {
1401
+ await this.start()
1402
+ }
1403
+
1404
+ // Separate assistant-level options from conversation-level options
1405
+ const { history: historyMode, forbidTools, allowTools, toolNames, onFork, ...convOverrides } = options
1406
+
1407
+ // Fork the conversation with history truncation
1408
+ const forkedConv = this.conversation.fork({ history: historyMode ?? 'full', ...convOverrides })
1409
+
1410
+ // Create a new assistant that reuses the forked conversation
1411
+ const forkedAssistant = this.container.feature('assistant', {
1412
+ ...this.options,
1413
+ // Pass through conversation overrides that map to assistant options
1414
+ ...(convOverrides.model ? { model: convOverrides.model } : {}),
1415
+ ...(convOverrides.maxTokens ? { maxTokens: convOverrides.maxTokens } : {}),
1416
+ ...(convOverrides.temperature != null ? { temperature: convOverrides.temperature } : {}),
1417
+ ...(convOverrides.topP != null ? { topP: convOverrides.topP } : {}),
1418
+ ...(convOverrides.topK != null ? { topK: convOverrides.topK } : {}),
1419
+ ...(convOverrides.frequencyPenalty != null ? { frequencyPenalty: convOverrides.frequencyPenalty } : {}),
1420
+ ...(convOverrides.presencePenalty != null ? { presencePenalty: convOverrides.presencePenalty } : {}),
1421
+ ...(convOverrides.stop ? { stop: convOverrides.stop } : {}),
1422
+ // Pass through tool filtering options
1423
+ ...(forbidTools ? { forbidTools } : {}),
1424
+ ...(allowTools ? { allowTools } : {}),
1425
+ ...(toolNames ? { toolNames } : {}),
1426
+ }) as Assistant
1427
+
1428
+ // Inject the forked conversation directly, bypassing the lazy getter
1429
+ forkedAssistant.state.set('conversation', forkedConv)
1430
+
1431
+ // Track fork depth so forks know they are forks
1432
+ forkedAssistant.state.set('forkDepth', this.forkDepth + 1)
1433
+
1434
+ // Clone interceptors so the fork behaves like the original
1435
+ forkedAssistant.interceptors.beforeAsk = this.interceptors.beforeAsk.clone()
1436
+ forkedAssistant.interceptors.beforeTurn = this.interceptors.beforeTurn.clone()
1437
+ forkedAssistant.interceptors.beforeToolCall = this.interceptors.beforeToolCall.clone()
1438
+ forkedAssistant.interceptors.afterToolCall = this.interceptors.afterToolCall.clone()
1439
+ forkedAssistant.interceptors.beforeResponse = this.interceptors.beforeResponse.clone()
1440
+
1441
+ // Copy system prompt extensions
1442
+ forkedAssistant.state.set('systemPromptExtensions', { ...this.systemPromptExtensions })
1443
+
1444
+ // Start wires up event forwarding and the interceptor-aware tool executor
1445
+ await forkedAssistant.start()
1446
+
1447
+ // Call the onFork hook if provided — lets callers customize the fork before use
1448
+ if (onFork) {
1449
+ await onFork(forkedAssistant, this)
1450
+ }
1451
+
1452
+ return forkedAssistant
1453
+ }
1454
+
1455
+ /** Active and completed research jobs, keyed by job entity ID. */
1456
+ readonly researchJobs = new Map<string, ResearchJob>()
1457
+
1458
+ /**
1459
+ * Create a non-blocking research job that fans out questions across forked assistants.
1460
+ * The forks fire immediately and the returned entity tracks progress via observable
1461
+ * state and events. Each fork preserves the full assistant identity (interceptors,
1462
+ * tools, hooks).
1463
+ *
1464
+ * @param prompt - Shared context/framing prompt prepended to each fork's system prompt
1465
+ * @param questions - Array of questions (strings) or objects with question + per-fork overrides
1466
+ * @param defaults - Default fork options applied to all forks
1467
+ * @returns A research job entity with observable state and events
1468
+ *
1469
+ * @example
1470
+ * ```typescript
1471
+ * // Fire and forget — check later
1472
+ * const job = await assistant.createResearchJob(
1473
+ * "Analyze this codebase for security issues",
1474
+ * ["Look for SQL injection", "Look for XSS", "Look for auth bypass"],
1475
+ * { history: 'none', model: 'gpt-4o-mini' }
1476
+ * )
1477
+ *
1478
+ * // Check progress
1479
+ * job.state.get('completed') // 2 of 3
1480
+ * job.state.get('results') // [answer1, answer2, null]
1481
+ *
1482
+ * // React to events
1483
+ * job.on('forkCompleted', (index, result) => console.log(`Fork ${index} done`))
1484
+ *
1485
+ * // Or just wait
1486
+ * await job.waitFor('completed')
1487
+ * ```
1488
+ */
1489
+ async createResearchJob(
1490
+ prompt: string,
1491
+ questions: (string | { question: string; forkOptions?: AssistantForkOptions })[],
1492
+ defaults: AssistantForkOptions = {}
1493
+ ): Promise<ResearchJob> {
1494
+ if (!this.isStarted) {
1495
+ await this.start()
1496
+ }
1497
+
1498
+ const jobId = `research:${this.container.utils.uuid()}`
1499
+ const total = questions.length
1500
+
1501
+ const job = this.container.entity<ResearchJobState, ResearchJobOptions, ResearchJobEvents>(
1502
+ jobId,
1503
+ { prompt, questions: questions.map(q => typeof q === 'string' ? q : q.question), forkOptions: defaults },
1504
+ ) as ResearchJob
1505
+
1506
+ job.setState({
1507
+ status: 'running',
1508
+ prompt,
1509
+ questions: questions.map(q => typeof q === 'string' ? q : q.question),
1510
+ results: new Array(total).fill(null),
1511
+ errors: new Array(total).fill(null),
1512
+ completed: 0,
1513
+ total,
1514
+ })
1515
+
1516
+ this.researchJobs.set(jobId, job)
1517
+
1518
+ // Build fork configs and create forks
1519
+ const forkConfigs = questions.map(q => ({
1520
+ ...defaults,
1521
+ ...(typeof q === 'string' ? {} : q.forkOptions),
1522
+ }))
1523
+
1524
+ const forks = await this.fork(forkConfigs)
1525
+
1526
+ // Apply shared prompt as a system prompt extension on each fork
1527
+ if (prompt) {
1528
+ for (const fork of forks) {
1529
+ fork.addSystemPromptExtension('researchPrompt', prompt)
1530
+ }
1531
+ }
1532
+
1533
+ // Fire all forks — don't await the batch, let them resolve individually
1534
+ for (let i = 0; i < forks.length; i++) {
1535
+ const fork = forks[i]!
1536
+ const q = questions[i]!
1537
+ const question = typeof q === 'string' ? q : q.question
1538
+
1539
+ fork.ask(question).then(
1540
+ (result) => {
1541
+ const results = [...job.state.get('results')!]
1542
+ results[i] = result
1543
+ const completed = job.state.get('completed')! + 1
1544
+
1545
+ job.setState({ results, completed })
1546
+ job.emit('forkCompleted', i, result)
1547
+
1548
+ if (completed === total) {
1549
+ job.setState({ status: 'completed' })
1550
+ job.emit('completed', results as string[])
1551
+ }
1552
+ },
1553
+ (err) => {
1554
+ const errors = [...job.state.get('errors')!]
1555
+ errors[i] = err?.message || String(err)
1556
+ const completed = job.state.get('completed')! + 1
1557
+
1558
+ job.setState({ errors, completed })
1559
+ job.emit('forkError', i, errors[i]!)
1560
+
1561
+ if (completed === total) {
1562
+ const results = job.state.get('results')!
1563
+ const hasAnyResult = results.some(r => r !== null)
1564
+ job.setState({ status: hasAnyResult ? 'completed' : 'failed' })
1565
+
1566
+ if (hasAnyResult) {
1567
+ job.emit('completed', results as string[])
1568
+ } else {
1569
+ job.emit('failed', errors)
1570
+ }
1571
+ }
1572
+ }
1573
+ )
1574
+ }
1575
+
1576
+ return job
1577
+ }
1578
+
1579
+ /**
1580
+ * Fan out N questions in parallel using forked assistants, return the results.
1581
+ * Sugar over createResearchJob — blocks until all forks complete.
1582
+ *
1583
+ * @param questions - Array of questions (strings) or objects with question + per-fork overrides
1584
+ * @param defaults - Default fork options applied to all forks
1585
+ * @returns Array of response strings, one per question
1586
+ *
1587
+ * @example
1588
+ * ```typescript
1589
+ * const results = await assistant.research([
1590
+ * "What are best practices for X?",
1591
+ * "What are common pitfalls of X?",
1592
+ * ], { history: 'none', model: 'gpt-4o-mini' })
1593
+ * ```
1594
+ */
1595
+ async research(
1596
+ questions: (string | { question: string; forkOptions?: AssistantForkOptions })[],
1597
+ defaults: AssistantForkOptions & { prompt?: string } = {}
1598
+ ): Promise<(string | null)[]> {
1599
+ const { prompt = '', ...forkDefaults } = defaults
1600
+ const job = await this.createResearchJob(prompt, questions, forkDefaults)
1601
+ await job.waitFor('completed')
1602
+ return job.state.get('results')!
1603
+ }
1604
+
1605
+ // -- Subagent API --
1606
+
1607
+ /**
1608
+ * Names of assistants available as subagents, discovered via the assistantsManager.
1609
+ *
1610
+ * @returns {string[]} Available assistant names
1611
+ */
1612
+ get availableSubagents(): string[] {
1613
+ try {
1614
+ const manager = this.container.feature('assistantsManager')
1615
+ return manager.available
1616
+ } catch {
1617
+ return []
1618
+ }
1619
+ }
1620
+
1621
+ /**
1622
+ * Get or create a subagent assistant. Uses the assistantsManager to discover
1623
+ * and create the assistant, then caches the instance for reuse across tool calls.
1624
+ *
1625
+ * @param id - The assistant name (e.g. 'codingAssistant')
1626
+ * @param options - Additional options to pass to the assistant constructor
1627
+ * @returns {Promise<Assistant>} The subagent assistant instance, started and ready
1628
+ *
1629
+ * @example
1630
+ * ```typescript
1631
+ * const researcher = await assistant.subagent('codingAssistant')
1632
+ * const answer = await researcher.ask('Find all usages of container.feature("fs")')
1633
+ * ```
1634
+ */
1635
+ async subagent(id: string, options: Record<string, any> = {}): Promise<Assistant> {
1636
+ const subagents = (this.state.get('subagents') || {}) as Record<string, Assistant>
1637
+ if (subagents[id]) return subagents[id]
1638
+
1639
+ const manager = this.container.feature('assistantsManager')
1640
+
1641
+ if (!manager.state.get('discovered')) {
1642
+ await manager.discover()
1643
+ }
1644
+
1645
+ const instance = manager.create(id, options)
1646
+ await instance.start()
1647
+
1648
+ this.state.set('subagents', { ...subagents, [id]: instance })
1649
+ return instance
1650
+ }
1651
+ }
1652
+
1653
+ export default Assistant