@soederpop/luca 0.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 (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,767 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import type { Container } from '@soederpop/luca/container'
4
+ import { type AvailableFeatures } from '@soederpop/luca/feature'
5
+ import { features, Feature } from '@soederpop/luca/feature'
6
+ import type { Conversation, ConversationTool, ContentPart, AskOptions, Message } from './conversation'
7
+ import type { AGIContainer } from '../container.server.js'
8
+ import type { ContentDb } from '@soederpop/luca/node'
9
+ import type { ConversationHistory, ConversationMeta } from './conversation-history'
10
+ import hashObject from '../../hash-object.js'
11
+
12
+ declare module '@soederpop/luca/feature' {
13
+ interface AvailableFeatures {
14
+ assistant: typeof Assistant
15
+ }
16
+ }
17
+
18
+ export const AssistantEventsSchema = FeatureEventsSchema.extend({
19
+ created: z.tuple([]).describe('Emitted immediately after the assistant loads its prompt, tools, and hooks.'),
20
+ started: z.tuple([]).describe('Emitted when the assistant has been initialized'),
21
+ 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'),
22
+ 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'),
23
+ chunk: z.tuple([z.string().describe('A chunk of streamed text')]).describe('Emitted as tokens stream in'),
24
+ preview: z.tuple([z.string().describe('The accumulated response so far')]).describe('Emitted with the full response text accumulated across all turns'),
25
+ response: z.tuple([z.string().describe('The final response text')]).describe('Emitted when a complete response is produced (accumulated across all turns)'),
26
+ 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'),
27
+ 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'),
28
+ toolCall: z.tuple([z.string().describe('Tool name'), z.any().describe('Tool arguments')]).describe('Emitted when a tool is called'),
29
+ toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
30
+ toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
31
+ hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
32
+ })
33
+
34
+ export const AssistantStateSchema = FeatureStateSchema.extend({
35
+ started: z.boolean().describe('Whether the assistant has been initialized'),
36
+ conversationCount: z.number().describe('Number of ask() calls made'),
37
+ lastResponse: z.string().describe('The most recent response text'),
38
+ folder: z.string().describe('The resolved assistant folder path'),
39
+ docsFolder: z.string().describe('The resolved docs folder'),
40
+ conversationId: z.string().optional().describe('The active conversation persistence ID'),
41
+ threadId: z.string().optional().describe('The active thread ID'),
42
+ })
43
+
44
+ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
45
+ /** The folder containing the assistant definition (CORE.md, tools.ts, hooks.ts) */
46
+ folder: z.string().describe('The folder containing the assistant definition'),
47
+
48
+ /** If the docs folder is different from folder/docs */
49
+ docsFolder: z.string().optional().describe('The folder containing the assistant documentation'),
50
+
51
+ /** Text to prepend to the system prompt from CORE.md */
52
+ prependPrompt: z.string().optional().describe('Text to prepend to the system prompt'),
53
+
54
+ /** Text to append to the system prompt from CORE.md */
55
+ appendPrompt: z.string().optional().describe('Text to append to the system prompt'),
56
+ /** Override or extend the tools loaded from tools.ts */
57
+
58
+ tools: z.record(z.string(), z.any()).optional().describe('Override or extend the tools loaded from tools.ts'),
59
+ /** Override or extend the schemas loaded from tools.ts */
60
+
61
+ schemas: z.record(z.string(), z.any()).optional().describe('Override or extend schemas whose keys match tool names'),
62
+ /** OpenAI model to use for the conversation */
63
+
64
+ model: z.string().optional().describe('OpenAI model to use'),
65
+ /** Maximum number of output tokens per completion */
66
+
67
+ maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
68
+
69
+ /** History persistence mode: lifecycle (ephemeral), daily (auto-resume per day), persistent (single long-running thread), session (unique per run, resumable) */
70
+ historyMode: z.enum(['lifecycle', 'daily', 'persistent', 'session']).optional().describe('Conversation history persistence mode'),
71
+ })
72
+
73
+ export type AssistantState = z.infer<typeof AssistantStateSchema>
74
+ export type AssistantOptions = z.infer<typeof AssistantOptionsSchema>
75
+
76
+ /**
77
+ * An Assistant is a combination of a system prompt and tool calls that has a
78
+ * conversation with an LLM. You define an assistant by creating a folder with
79
+ * CORE.md (system prompt), tools.ts (tool implementations), and hooks.ts (event handlers).
80
+ *
81
+ * @extends Feature
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const assistant = container.feature('assistant', {
86
+ * folder: 'assistants/my-helper'
87
+ * })
88
+ * const answer = await assistant.ask('What capabilities do you have?')
89
+ * ```
90
+ */
91
+ export class Assistant extends Feature<AssistantState, AssistantOptions> {
92
+ static override stateSchema = AssistantStateSchema
93
+ static override optionsSchema = AssistantOptionsSchema
94
+ static override eventsSchema = AssistantEventsSchema
95
+ static override shortcut = 'features.assistant' as const
96
+
97
+ static attach(container: Container<AvailableFeatures, any>) {
98
+ features.register('assistant', Assistant)
99
+ return container
100
+ }
101
+
102
+ /** @returns Default state with the assistant not started, zero conversations, and the resolved folder path. */
103
+ override get initialState(): AssistantState {
104
+ return {
105
+ ...super.initialState,
106
+ started: false,
107
+ conversationCount: 0,
108
+ lastResponse: '',
109
+ folder: this.resolvedFolder,
110
+ }
111
+ }
112
+
113
+ override get container(): AGIContainer {
114
+ return super.container as AGIContainer
115
+ }
116
+
117
+ /** The absolute resolved path to the assistant folder. */
118
+ get resolvedFolder(): string {
119
+ return this.container.paths.resolve(this.options.folder)
120
+ }
121
+
122
+ /** The path to CORE.md which provides the system prompt. */
123
+ get corePromptPath(): string {
124
+ return this.paths.resolve('CORE.md')
125
+ }
126
+
127
+ /** The path to tools.ts which provides tool implementations and schemas. */
128
+ get toolsModulePath(): string {
129
+ return this.paths.resolve('tools.ts')
130
+ }
131
+
132
+ /** The path to hooks.ts which provides event handler functions. */
133
+ get hooksModulePath(): string {
134
+ return this.paths.resolve('hooks.ts')
135
+ }
136
+
137
+ /** Whether this assistant has a voice.yaml configuration file. */
138
+ get hasVoice(): boolean {
139
+ return this.container.fs.exists(this.paths.resolve('voice.yaml'))
140
+ }
141
+
142
+ /** Parsed voice configuration from voice.yaml, or undefined if not present. */
143
+ get voiceConfig(): Record<string, any> | undefined {
144
+ if (!this.hasVoice) return undefined
145
+ const yaml = this.container.feature('yaml')
146
+ return yaml.parse(this.container.fs.readFile(this.paths.resolve('voice.yaml')))
147
+ }
148
+
149
+ get resolvedDocsFolder() {
150
+ const { docsFolder = this.options.docsFolder || 'docs' } = this.state.current
151
+
152
+ if (this.container.fs.exists(docsFolder)) {
153
+ return this.container.paths.resolve(docsFolder)
154
+ }
155
+
156
+ const findUp = this.container.fs.findUp('docs', {
157
+ cwd: this.resolvedFolder
158
+ })
159
+
160
+ if (typeof findUp === 'string' && this.container.fs.exists(findUp!)) {
161
+ this.state.set('docsFolder', findUp!)
162
+ return this.container.paths.resolve(findUp!)
163
+ }
164
+
165
+ return this.paths.resolve('docs')
166
+ }
167
+
168
+ /**
169
+ * Returns an instance of a ContentDb feature for the resolved docs folder
170
+ */
171
+ get contentDb() : ContentDb {
172
+ return this.container.feature('contentDb', { rootPath: this.resolvedDocsFolder })
173
+ }
174
+
175
+ private _conversation?: Conversation
176
+ private _resumeThreadId?: string
177
+
178
+ // Using `declare` to prevent class field initializers from overwriting
179
+ // values set during afterInitialize() (called from the base constructor).
180
+ declare private _tools: Record<string, ConversationTool>
181
+ declare private _hooks: Record<string, (...args: any[]) => any>
182
+ declare private _systemPrompt: string
183
+ declare private _pendingPlugins: Promise<void>[]
184
+
185
+ /**
186
+ * Called immediately after the assistant is constructed. Synchronously loads
187
+ * the system prompt, tools, and hooks, then binds hooks as event listeners
188
+ * so every emitted event automatically invokes its corresponding hook.
189
+ */
190
+ override afterInitialize() {
191
+ this._pendingPlugins = []
192
+
193
+ // Load system prompt synchronously
194
+ this._systemPrompt = this.loadSystemPrompt()
195
+
196
+ // Load tools and hooks synchronously via vm.performSync
197
+ this._tools = this.loadTools()
198
+ this._hooks = this.loadHooks()
199
+
200
+ // Bind hooks to events BEFORE emitting created so the created hook fires
201
+ this.bindHooksToEvents()
202
+
203
+ this.emit('created')
204
+ }
205
+
206
+ get conversation(): Conversation {
207
+ if (!this._conversation) {
208
+ this._conversation = this.container.feature('conversation', {
209
+ model: this.options.model || 'gpt-5.2',
210
+ tools: this._tools || this.loadTools(),
211
+ ...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
212
+ history: [
213
+ { role: 'system', content: this._systemPrompt || this.loadSystemPrompt() },
214
+ ],
215
+ })
216
+ }
217
+ return this._conversation
218
+ }
219
+
220
+ get messages() {
221
+ return this.conversation.messages
222
+ }
223
+
224
+ /** Whether the assistant has been started and is ready to receive questions. */
225
+ get isStarted(): boolean {
226
+ return !!this.state.get('started')
227
+ }
228
+
229
+ /** The current system prompt text. */
230
+ get systemPrompt(): string {
231
+ return this._systemPrompt
232
+ }
233
+
234
+ /** The tools registered with this assistant. */
235
+ get tools(): Record<string, ConversationTool> {
236
+ return this._tools
237
+ }
238
+
239
+ /**
240
+ * Apply a setup function to this assistant. The function receives the
241
+ * assistant instance and can configure tools, hooks, event listeners, etc.
242
+ *
243
+ * @param fn - Setup function that receives this assistant
244
+ * @returns this, for chaining
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * assistant
249
+ * .use(setupLogging)
250
+ * .use(addAnalyticsTools)
251
+ * ```
252
+ */
253
+ use(fn: (assistant: this) => void | Promise<void>): this {
254
+ const result = fn(this)
255
+ if (result && typeof (result as any).then === 'function') {
256
+ this._pendingPlugins.push(result as Promise<void>)
257
+ }
258
+ return this
259
+ }
260
+
261
+ /**
262
+ * Add a tool to this assistant. The tool name is derived from the
263
+ * handler's function name.
264
+ *
265
+ * @param handler - A named function that implements the tool
266
+ * @param schema - Optional Zod schema describing the tool's parameters
267
+ * @returns this, for chaining
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * assistant.addTool(function getWeather(args) {
272
+ * return { temp: 72 }
273
+ * }, z.object({ city: z.string() }).describe('Get weather for a city'))
274
+ * ```
275
+ */
276
+ addTool(handler: (...args: any[]) => any, schema?: z.ZodType): this {
277
+ const name = handler.name
278
+ if (!name) throw new Error('addTool handler must be a named function')
279
+
280
+ if (schema) {
281
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
282
+ this._tools[name] = {
283
+ handler: handler as ConversationTool['handler'],
284
+ description: jsonSchema.description || name,
285
+ parameters: {
286
+ type: jsonSchema.type || 'object',
287
+ properties: jsonSchema.properties || {},
288
+ ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
289
+ },
290
+ }
291
+ } else {
292
+ this._tools[name] = {
293
+ handler: handler as ConversationTool['handler'],
294
+ description: name,
295
+ parameters: { type: 'object', properties: {} },
296
+ }
297
+ }
298
+
299
+ return this
300
+ }
301
+
302
+ /**
303
+ * Remove a tool by name or handler function reference.
304
+ *
305
+ * @param nameOrHandler - The tool name string, or the handler function to match
306
+ * @returns this, for chaining
307
+ */
308
+ removeTool(nameOrHandler: string | ((...args: any[]) => any)): this {
309
+ if (typeof nameOrHandler === 'string') {
310
+ delete this._tools[nameOrHandler]
311
+ } else {
312
+ for (const [name, tool] of Object.entries(this._tools)) {
313
+ if (tool.handler === nameOrHandler) {
314
+ delete this._tools[name]
315
+ break
316
+ }
317
+ }
318
+ }
319
+
320
+ return this
321
+ }
322
+
323
+ /**
324
+ * Simulate a tool call and its result by appending the appropriate
325
+ * messages to the conversation history. Useful for injecting context
326
+ * that looks like the assistant performed a tool call.
327
+ *
328
+ * @param toolCallName - The name of the tool
329
+ * @param args - The arguments that were "passed" to the tool
330
+ * @param result - The result the tool "returned"
331
+ * @returns this, for chaining
332
+ */
333
+ simulateToolCallWithResult(toolCallName: string, args: Record<string, any>, result: any): this {
334
+ if (!this.conversation) {
335
+ throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
336
+ }
337
+
338
+ const callId = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
339
+
340
+ this.conversation.pushMessage({
341
+ role: 'assistant',
342
+ content: null,
343
+ tool_calls: [{
344
+ id: callId,
345
+ type: 'function',
346
+ function: {
347
+ name: toolCallName,
348
+ arguments: JSON.stringify(args),
349
+ },
350
+ }],
351
+ } as Message)
352
+
353
+ this.conversation.pushMessage({
354
+ role: 'tool',
355
+ tool_call_id: callId,
356
+ content: typeof result === 'string' ? result : JSON.stringify(result),
357
+ } as Message)
358
+
359
+ return this
360
+ }
361
+
362
+ /**
363
+ * Simulate a user question and assistant response by appending both
364
+ * messages to the conversation history.
365
+ *
366
+ * @param question - The user's question
367
+ * @param response - The assistant's response
368
+ * @returns this, for chaining
369
+ */
370
+ simulateQuestionAndResponse(question: string, response: string): this {
371
+ if (!this.conversation) {
372
+ throw new Error('Cannot simulate: assistant has no active conversation. Call start() first.')
373
+ }
374
+
375
+ this.conversation.pushMessage({ role: 'user', content: question })
376
+ this.conversation.pushMessage({ role: 'assistant', content: response })
377
+
378
+ return this
379
+ }
380
+
381
+ /**
382
+ * Load the system prompt from CORE.md, applying any prepend/append options.
383
+ *
384
+ * @returns {string} The assembled system prompt
385
+ */
386
+ loadSystemPrompt(): string {
387
+ const { fs } = this.container
388
+ let prompt = ''
389
+
390
+ if (fs.exists(this.corePromptPath)) {
391
+ prompt = fs.readFile(this.corePromptPath)
392
+ }
393
+
394
+ if (this.options.prependPrompt) {
395
+ prompt = this.options.prependPrompt + '\n\n' + prompt
396
+ }
397
+
398
+ if (this.options.appendPrompt) {
399
+ prompt = prompt + '\n\n' + this.options.appendPrompt
400
+ }
401
+
402
+ return prompt.trim()
403
+ }
404
+
405
+ /**
406
+ * Load tools from tools.ts using the container's VM feature, injecting
407
+ * the container and assistant as globals. Merges with any tools
408
+ * provided in the constructor options. Runs synchronously via vm.loadModule.
409
+ *
410
+ * @returns {Record<string, ConversationTool>} The assembled tool map
411
+ */
412
+ loadTools(): Record<string, ConversationTool> {
413
+ const tools: Record<string, ConversationTool> = {}
414
+ const vm = this.container.feature('vm')
415
+
416
+ let moduleExports: Record<string, any>
417
+ try {
418
+ moduleExports = vm.loadModule(this.toolsModulePath, {
419
+ container: this.container,
420
+ me: this,
421
+ my: this,
422
+ assistant: this,
423
+ console: console,
424
+ })
425
+ } catch (err: any) {
426
+ console.error(`Failed to load tools from ${this.toolsModulePath}`)
427
+ console.error(`There may be a syntax error in this file. Please check it.`)
428
+ console.error(err.message || err)
429
+ return tools
430
+ }
431
+
432
+ if (Object.keys(moduleExports).length) {
433
+ const schemas: Record<string, z.ZodType> = moduleExports.schemas || {}
434
+
435
+ for (const [name, fn] of Object.entries(moduleExports)) {
436
+ if (name === 'schemas' || name === 'default' || typeof fn !== 'function') continue
437
+
438
+ const schema = schemas[name]
439
+ if (schema) {
440
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
441
+ tools[name] = {
442
+ handler: fn as ConversationTool['handler'],
443
+ description: jsonSchema.description || name,
444
+ parameters: {
445
+ type: jsonSchema.type || 'object',
446
+ properties: jsonSchema.properties || {},
447
+ ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
448
+ },
449
+ }
450
+ } else {
451
+ tools[name] = {
452
+ handler: fn as ConversationTool['handler'],
453
+ description: name,
454
+ parameters: { type: 'object', properties: {} },
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ // Merge in option-provided tools and schemas
461
+ if (this.options.tools) {
462
+ const optionSchemas = this.options.schemas || {}
463
+
464
+ for (const [name, fn] of Object.entries(this.options.tools)) {
465
+ if (typeof fn !== 'function') continue
466
+
467
+ const schema = optionSchemas[name]
468
+ if (schema) {
469
+ const jsonSchema = (schema as any).toJSONSchema() as Record<string, any>
470
+ tools[name] = {
471
+ handler: fn as ConversationTool['handler'],
472
+ description: jsonSchema.description || name,
473
+ parameters: {
474
+ type: jsonSchema.type || 'object',
475
+ properties: jsonSchema.properties || {},
476
+ ...(jsonSchema.required ? { required: jsonSchema.required } : {}),
477
+ },
478
+ }
479
+ } else {
480
+ tools[name] = {
481
+ handler: fn as ConversationTool['handler'],
482
+ description: name,
483
+ parameters: { type: 'object', properties: {} },
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ return tools
490
+ }
491
+
492
+ /**
493
+ * Load event hooks from hooks.ts. Each exported function name should
494
+ * match an event the assistant emits. When that event fires, the
495
+ * corresponding hook function is called. Runs synchronously via vm.loadModule.
496
+ *
497
+ * @returns {Record<string, Function>} The hook function map
498
+ */
499
+ loadHooks(): Record<string, (...args: any[]) => any> {
500
+ const hooks: Record<string, (...args: any[]) => any> = {}
501
+ const vm = this.container.feature('vm')
502
+
503
+ let moduleExports: Record<string, any>
504
+ try {
505
+ moduleExports = vm.loadModule(this.hooksModulePath, {
506
+ container: this.container,
507
+ me: this,
508
+ my: this,
509
+ assistant: this,
510
+ console: console,
511
+ })
512
+ } catch (err: any) {
513
+ console.error(`Failed to load hooks from ${this.hooksModulePath}`)
514
+ console.error(`There may be a syntax error in this file. Please check it.`)
515
+ console.error(err.message || err)
516
+ return hooks
517
+ }
518
+
519
+ for (const [name, fn] of Object.entries(moduleExports)) {
520
+ if (name === 'default' || typeof fn !== 'function') continue
521
+ hooks[name] = fn as (...args: any[]) => any
522
+ }
523
+
524
+ return hooks
525
+ }
526
+
527
+ /**
528
+ * Provides a helper for creating paths off of the assistant's base folder
529
+ */
530
+ get paths() {
531
+ const { container } = this
532
+ const base = this.resolvedFolder
533
+
534
+ return {
535
+ resolve(...args: any[]) {
536
+ return container.paths.resolve(base, ...args)
537
+ },
538
+ join(...args: any[]) {
539
+ return container.paths.resolve(base, ...args)
540
+ }
541
+ }
542
+ }
543
+
544
+ // -- History mode helpers --
545
+
546
+ /** The assistant name derived from the folder basename. */
547
+ get assistantName(): string {
548
+ return this.resolvedFolder.split('/').pop() || 'assistant'
549
+ }
550
+
551
+ /** An 8-char hash of the container cwd for per-project thread isolation. */
552
+ get cwdHash(): string {
553
+ return hashObject(this.container.cwd).slice(0, 8)
554
+ }
555
+
556
+ /** The thread prefix for this assistant+project combination. */
557
+ get threadPrefix(): string {
558
+ return `${this.assistantName}:${this.cwdHash}:`
559
+ }
560
+
561
+ /** Build a thread ID based on the history mode. */
562
+ private buildThreadId(mode: string): string {
563
+ const prefix = this.threadPrefix
564
+ switch (mode) {
565
+ case 'daily': {
566
+ const today = new Date().toISOString().slice(0, 10)
567
+ return `${prefix}${today}`
568
+ }
569
+ case 'persistent':
570
+ return `${prefix}persistent`
571
+ case 'session':
572
+ return `${prefix}${this.uuid}`
573
+ default:
574
+ return `${prefix}${this.uuid}`
575
+ }
576
+ }
577
+
578
+ /** The conversationHistory feature instance. */
579
+ get conversationHistory(): ConversationHistory {
580
+ return this.container.feature('conversationHistory') as ConversationHistory
581
+ }
582
+
583
+ /** The active thread ID (undefined in lifecycle mode). */
584
+ get currentThreadId(): string | undefined {
585
+ return this.state.get('threadId')
586
+ }
587
+
588
+ /**
589
+ * Override thread for resume. Call before start().
590
+ *
591
+ * @param threadId - The thread ID to resume
592
+ * @returns this, for chaining
593
+ */
594
+ resumeThread(threadId: string): this {
595
+ this._resumeThreadId = threadId
596
+ return this
597
+ }
598
+
599
+ /**
600
+ * List saved conversations for this assistant+project.
601
+ *
602
+ * @param opts - Optional limit
603
+ * @returns Conversation metadata records
604
+ */
605
+ async listHistory(opts?: { limit?: number }): Promise<ConversationMeta[]> {
606
+ const metas = await this.conversationHistory.findByThreadPrefix(this.threadPrefix)
607
+ if (opts?.limit) return metas.slice(0, opts.limit)
608
+ return metas
609
+ }
610
+
611
+ /**
612
+ * Delete all history for this assistant+project.
613
+ *
614
+ * @returns Number of conversations deleted
615
+ */
616
+ async clearHistory(): Promise<number> {
617
+ return this.conversationHistory.deleteByThreadPrefix(this.threadPrefix)
618
+ }
619
+
620
+ /**
621
+ * Load history into the conversation after it's been created.
622
+ * Called from start() for non-lifecycle modes.
623
+ */
624
+ private async loadConversationHistory(): Promise<void> {
625
+ const mode = this.options.historyMode || 'lifecycle'
626
+ if (mode === 'lifecycle') return
627
+
628
+ const threadId = this._resumeThreadId || this.buildThreadId(mode)
629
+ this.state.set('threadId', threadId)
630
+
631
+ const existing = await this.conversationHistory.findByThread(threadId)
632
+
633
+ if (existing) {
634
+ // Replace conversation messages with loaded history
635
+ const messages = [...existing.messages]
636
+
637
+ // Swap in fresh system prompt if it changed
638
+ if (messages.length > 0 && (messages[0]!.role === 'system' || messages[0]!.role === 'developer')) {
639
+ messages[0] = { role: messages[0]!.role, content: this._systemPrompt }
640
+ }
641
+
642
+ this.conversation.state.set('id', existing.id)
643
+ this.conversation.state.set('thread', threadId)
644
+ this.conversation.state.set('messages', messages)
645
+ this.state.set('conversationId', existing.id)
646
+ } else {
647
+ // Fresh conversation — just set thread
648
+ this.conversation.state.set('thread', threadId)
649
+ this.state.set('conversationId', this.conversation.state.get('id'))
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Bind all loaded hook functions as event listeners. Each hook whose
655
+ * name matches an event gets wired up so it fires automatically when
656
+ * that event is emitted. Must be called before any events are emitted.
657
+ */
658
+ private bindHooksToEvents() {
659
+ const assistant = this
660
+ for (const [eventName, hookFn] of Object.entries(this._hooks)) {
661
+ this.on(eventName as any, (...args: any[]) => {
662
+ this.emit('hookFired', eventName)
663
+ hookFn(assistant, ...args)
664
+ })
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Start the assistant by creating the conversation and wiring up events.
670
+ * The system prompt, tools, and hooks are already loaded synchronously
671
+ * during initialization.
672
+ *
673
+ * @returns {Promise<this>} The initialized assistant
674
+ */
675
+ async start(): Promise<this> {
676
+ // Prevent duplicate listener registration if already started
677
+ if (this.isStarted) return this
678
+
679
+ // Wait for any async .use() plugins to finish before starting
680
+ if (this._pendingPlugins.length) {
681
+ await Promise.all(this._pendingPlugins)
682
+ this._pendingPlugins = []
683
+ }
684
+
685
+ // Wire up event forwarding from conversation to assistant.
686
+ // Hooks fire automatically because they're bound as event listeners.
687
+ this.conversation.on('turnStart', (info: any) => this.emit('turnStart', info))
688
+ this.conversation.on('turnEnd', (info: any) => this.emit('turnEnd', info))
689
+ this.conversation.on('chunk', (chunk: string) => this.emit('chunk', chunk))
690
+ this.conversation.on('preview', (text: string) => this.emit('preview', text))
691
+ this.conversation.on('response', (text: string) => {
692
+ this.emit('response', text)
693
+ this.state.set('lastResponse', text)
694
+ })
695
+ this.conversation.on('rawEvent', (event: any) => this.emit('rawEvent', event))
696
+ this.conversation.on('mcpEvent', (event: any) => this.emit('mcpEvent', event))
697
+ this.conversation.on('toolCall', (name: string, args: any) => this.emit('toolCall', name, args))
698
+ this.conversation.on('toolResult', (name: string, result: any) => this.emit('toolResult', name, result))
699
+ this.conversation.on('toolError', (name: string, error: any) => this.emit('toolError', name, error))
700
+
701
+ // Load conversation history for non-lifecycle modes
702
+ await this.loadConversationHistory()
703
+
704
+ // Enable autoCompact for modes that accumulate history
705
+ const mode = this.options.historyMode || 'lifecycle'
706
+ if (mode === 'daily' || mode === 'persistent') {
707
+ (this.conversation.options as any).autoCompact = true
708
+ }
709
+
710
+ this.state.set('started', true)
711
+ this.emit('started')
712
+
713
+ return this
714
+ }
715
+
716
+ /**
717
+ * Ask the assistant a question. It will use its tools to produce
718
+ * a streamed response. The assistant auto-starts if needed.
719
+ *
720
+ * @param {string | ContentPart[]} question - The question to ask
721
+ * @returns {Promise<string>} The assistant's response
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * const answer = await assistant.ask('What capabilities do you have?')
726
+ * ```
727
+ */
728
+ async ask(question: string | ContentPart[], options?: AskOptions): Promise<string> {
729
+ if (!this.isStarted) {
730
+ await this.start()
731
+ }
732
+
733
+ if (!this.conversation) {
734
+ return 'Assistant is not started'
735
+ }
736
+
737
+ const count = (this.state.get('conversationCount') || 0) + 1
738
+ this.state.set('conversationCount', count)
739
+
740
+ const result = await this.conversation.ask(question, options)
741
+
742
+ // Auto-save for non-lifecycle modes
743
+ if (this.options.historyMode !== 'lifecycle' && this.state.get('threadId')) {
744
+ await this.conversation.save({ thread: this.state.get('threadId') })
745
+ }
746
+
747
+ this.emit('answered', result)
748
+
749
+ return result
750
+ }
751
+
752
+ /**
753
+ * Save the conversation to disk via conversationHistory.
754
+ *
755
+ * @param opts - Optional overrides for title, tags, thread, or metadata
756
+ * @returns The saved conversation record
757
+ */
758
+ async save(opts?: { title?: string; tags?: string[]; thread?: string; metadata?: Record<string, any> }) {
759
+ if (!this.conversation) {
760
+ throw new Error('Cannot save: assistant has no active conversation')
761
+ }
762
+
763
+ return this.conversation.save(opts)
764
+ }
765
+ }
766
+
767
+ export default features.register('assistant', Assistant)