luca 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/.github/workflows/release.yaml +1 -0
  2. package/CLAUDE.md +10 -2
  3. package/README.md +130 -112
  4. package/assistants/codingAssistant/CORE.md +6 -1
  5. package/assistants/codingAssistant/hooks.ts +1 -1
  6. package/assistants/inkbot/hooks.ts +1 -1
  7. package/assistants/inkbot/tools.ts +1 -1
  8. package/bun.lock +264 -321
  9. package/commands/audit-docs.ts +2 -2
  10. package/commands/build-bootstrap.ts +2 -3
  11. package/commands/build-python-bridge.ts +2 -3
  12. package/commands/build-scaffolds.ts +2 -3
  13. package/commands/bundle-consumer-project.ts +521 -0
  14. package/commands/generate-api-docs.ts +2 -2
  15. package/commands/inkbot.ts +2 -2
  16. package/commands/release.ts +2 -2
  17. package/commands/social.ts +137 -0
  18. package/commands/try-all-challenges.ts +3 -3
  19. package/commands/try-challenge.ts +3 -3
  20. package/datasets/lora/agentic-loop-session-candidates.jsonl +91 -0
  21. package/datasets/lora/agentic-loop-session-curation-summary.json +123 -0
  22. package/datasets/lora/luca-session-candidates.jsonl +29 -0
  23. package/datasets/lora/luca-session-curation-summary.json +121 -0
  24. package/datasets/lora/review-batch-1.jsonl +30 -0
  25. package/datasets/lora/review-manifest.json +41 -0
  26. package/datasets/lora/review-queue.jsonl +120 -0
  27. package/datasets/lora/review-schema.json +134 -0
  28. package/datasets/lora/review-template.jsonl +2 -0
  29. package/datasets/lora/review-ui.html +725 -0
  30. package/dist/agi/container.server.d.ts +2 -2
  31. package/dist/agi/features/assistant.d.ts +2 -2
  32. package/dist/agi/features/assistants-manager.d.ts +1 -1
  33. package/dist/agi/features/autonomous-assistant.d.ts +1 -1
  34. package/dist/agi/features/browser-use.d.ts +1 -1
  35. package/dist/agi/features/claude-code.d.ts +1 -1
  36. package/dist/agi/features/conversation-history.d.ts +2 -2
  37. package/dist/agi/features/conversation.d.ts +1 -1
  38. package/dist/agi/features/docs-reader.d.ts +1 -1
  39. package/dist/agi/features/file-tools.d.ts +1 -1
  40. package/dist/agi/features/luca-coder.d.ts +1 -1
  41. package/dist/agi/features/openai-codex.d.ts +1 -1
  42. package/dist/agi/features/skills-library.d.ts +1 -1
  43. package/dist/clients/civitai/index.d.ts +4 -4
  44. package/dist/clients/client-template.d.ts +4 -4
  45. package/dist/clients/comfyui/index.d.ts +2 -2
  46. package/dist/clients/elevenlabs/index.d.ts +2 -2
  47. package/dist/clients/openai/index.d.ts +2 -2
  48. package/dist/clients/supabase/index.d.ts +3 -3
  49. package/dist/command.d.ts +1 -1
  50. package/dist/node/container.d.ts +1 -1
  51. package/dist/node/features/helpers.d.ts +3 -3
  52. package/dist/node/features/semantic-search.d.ts +1 -1
  53. package/dist/node/features/vm.d.ts +3 -3
  54. package/dist/node.d.ts +1 -1
  55. package/dist/scaffolds/generated.d.ts +1 -1
  56. package/dist/selector.d.ts +1 -1
  57. package/features/cipher-social.ts +493 -0
  58. package/index.html +217 -190
  59. package/luca.console.ts +1 -1
  60. package/package.json +7 -2
  61. package/public/index.html +217 -190
  62. package/public/slides-ai-native.html +1 -1
  63. package/public/slides-intro.html +2 -2
  64. package/scripts/curate-claude-sessions.ts +561 -0
  65. package/scripts/examples/ask-luca-expert.ts +1 -1
  66. package/scripts/examples/assistant-questions.ts +1 -1
  67. package/scripts/examples/excalidraw-expert.ts +1 -1
  68. package/scripts/examples/file-manager.ts +1 -1
  69. package/scripts/examples/ideas.ts +1 -1
  70. package/scripts/examples/interactive-chat.ts +1 -1
  71. package/scripts/examples/opening-a-web-browser.ts +1 -1
  72. package/scripts/examples/telegram-bot.ts +1 -1
  73. package/scripts/examples/using-assistant-with-mcp.ts +1 -1
  74. package/scripts/examples/using-claude-code.ts +1 -1
  75. package/scripts/examples/using-contentdb.ts +2 -2
  76. package/scripts/examples/using-conversations.ts +1 -1
  77. package/scripts/examples/using-disk-cache.ts +1 -1
  78. package/scripts/examples/using-docker-shell.ts +1 -1
  79. package/scripts/examples/using-elevenlabs.ts +1 -1
  80. package/scripts/examples/using-google-calendar.ts +1 -1
  81. package/scripts/examples/using-google-docs.ts +1 -1
  82. package/scripts/examples/using-google-drive.ts +1 -1
  83. package/scripts/examples/using-google-sheets.ts +1 -1
  84. package/scripts/examples/using-nlp.ts +1 -1
  85. package/scripts/examples/using-ollama.ts +1 -1
  86. package/scripts/examples/using-postgres.ts +1 -1
  87. package/scripts/examples/using-runpod.ts +1 -1
  88. package/scripts/examples/using-tts.ts +1 -1
  89. package/scripts/scaffold.ts +5 -5
  90. package/scripts/scratch.ts +1 -1
  91. package/scripts/test-assistant-hooks.ts +1 -1
  92. package/scripts/test-docs-reader.ts +1 -1
  93. package/src/agi/container.server.ts +6 -2
  94. package/src/agi/features/agent-memory.ts +25 -25
  95. package/src/agi/features/assistant.ts +34 -5
  96. package/src/agi/features/assistants-manager.ts +122 -6
  97. package/src/agi/features/autonomous-assistant.ts +1 -1
  98. package/src/agi/features/browser-use.ts +20 -1
  99. package/src/agi/features/claude-code.ts +51 -5
  100. package/src/agi/features/coding-tools.ts +1 -1
  101. package/src/agi/features/conversation-history.ts +181 -4
  102. package/src/agi/features/conversation.ts +186 -15
  103. package/src/agi/features/docs-reader.ts +2 -2
  104. package/src/agi/features/file-tools.ts +49 -2
  105. package/src/agi/features/luca-coder.ts +7 -5
  106. package/src/agi/features/mcp-bridge.ts +532 -0
  107. package/src/agi/features/openai-codex.ts +2 -2
  108. package/src/agi/features/skills-library.ts +131 -52
  109. package/src/agi/lib/token-counter.ts +80 -0
  110. package/src/bootstrap/generated.ts +56 -57
  111. package/src/browser.ts +1 -1
  112. package/src/cli/build-info.ts +2 -2
  113. package/src/cli/cli.ts +2 -2
  114. package/src/clients/civitai/index.ts +5 -5
  115. package/src/clients/client-template.ts +4 -4
  116. package/src/clients/comfyui/index.ts +4 -4
  117. package/src/clients/elevenlabs/index.ts +4 -4
  118. package/src/clients/openai/index.ts +7 -7
  119. package/src/clients/supabase/index.ts +4 -4
  120. package/src/clients/voicebox/index.ts +4 -4
  121. package/src/command.ts +2 -1
  122. package/src/commands/chat.ts +1 -0
  123. package/src/commands/eval.ts +2 -56
  124. package/src/commands/introspect.ts +1 -1
  125. package/src/commands/prompt.ts +41 -9
  126. package/src/container-describer.ts +8 -1
  127. package/src/container.ts +13 -0
  128. package/src/entity.ts +2 -2
  129. package/src/helper.ts +1 -1
  130. package/src/introspection/generated.agi.ts +29596 -27654
  131. package/src/introspection/generated.node.ts +20284 -19247
  132. package/src/introspection/generated.web.ts +605 -584
  133. package/src/introspection/scan.ts +11 -6
  134. package/src/node/container.ts +9 -1
  135. package/src/node/features/content-db.ts +39 -2
  136. package/src/node/features/display-result.ts +57 -0
  137. package/src/node/features/helpers.ts +46 -7
  138. package/src/node/features/python.ts +25 -19
  139. package/src/node/features/repl.ts +1 -1
  140. package/src/node/features/secure-shell.ts +11 -17
  141. package/src/node/features/semantic-search.ts +2 -2
  142. package/src/node/features/socket-repl.ts +336 -0
  143. package/src/node/features/telnyx-assistant-connector.ts +1206 -0
  144. package/src/node/features/transpiler.ts +2 -3
  145. package/src/node/features/ui.ts +5 -0
  146. package/src/node/features/vm.ts +20 -3
  147. package/src/node.ts +3 -3
  148. package/src/python/generated.ts +0 -1
  149. package/src/scaffolds/generated.ts +82 -83
  150. package/src/selector.ts +1 -1
  151. package/src/servers/express.ts +1 -1
  152. package/src/web/features/helpers.ts +22 -0
  153. package/tsconfig.json +12 -12
  154. package/docs/CLI.md +0 -335
  155. package/docs/CNAME +0 -1
  156. package/docs/README.md +0 -60
  157. package/docs/TABLE-OF-CONTENTS.md +0 -183
  158. package/docs/apis/clients/elevenlabs.md +0 -308
  159. package/docs/apis/clients/graph.md +0 -107
  160. package/docs/apis/clients/openai.md +0 -429
  161. package/docs/apis/clients/rest.md +0 -161
  162. package/docs/apis/clients/websocket.md +0 -174
  163. package/docs/apis/features/agi/assistant.md +0 -625
  164. package/docs/apis/features/agi/assistants-manager.md +0 -282
  165. package/docs/apis/features/agi/auto-assistant.md +0 -279
  166. package/docs/apis/features/agi/browser-use.md +0 -802
  167. package/docs/apis/features/agi/claude-code.md +0 -884
  168. package/docs/apis/features/agi/conversation-history.md +0 -364
  169. package/docs/apis/features/agi/conversation.md +0 -548
  170. package/docs/apis/features/agi/docs-reader.md +0 -99
  171. package/docs/apis/features/agi/file-tools.md +0 -163
  172. package/docs/apis/features/agi/luca-coder.md +0 -407
  173. package/docs/apis/features/agi/openai-codex.md +0 -396
  174. package/docs/apis/features/agi/openapi.md +0 -138
  175. package/docs/apis/features/agi/semantic-search.md +0 -387
  176. package/docs/apis/features/agi/skills-library.md +0 -239
  177. package/docs/apis/features/node/container-link.md +0 -192
  178. package/docs/apis/features/node/content-db.md +0 -450
  179. package/docs/apis/features/node/disk-cache.md +0 -379
  180. package/docs/apis/features/node/dns.md +0 -652
  181. package/docs/apis/features/node/docker.md +0 -706
  182. package/docs/apis/features/node/downloader.md +0 -81
  183. package/docs/apis/features/node/esbuild.md +0 -60
  184. package/docs/apis/features/node/file-manager.md +0 -191
  185. package/docs/apis/features/node/fs.md +0 -1217
  186. package/docs/apis/features/node/git.md +0 -371
  187. package/docs/apis/features/node/google-auth.md +0 -193
  188. package/docs/apis/features/node/google-calendar.md +0 -202
  189. package/docs/apis/features/node/google-docs.md +0 -173
  190. package/docs/apis/features/node/google-drive.md +0 -246
  191. package/docs/apis/features/node/google-mail.md +0 -214
  192. package/docs/apis/features/node/google-sheets.md +0 -194
  193. package/docs/apis/features/node/grep.md +0 -292
  194. package/docs/apis/features/node/helpers.md +0 -164
  195. package/docs/apis/features/node/ink.md +0 -334
  196. package/docs/apis/features/node/ipc-socket.md +0 -249
  197. package/docs/apis/features/node/json-tree.md +0 -86
  198. package/docs/apis/features/node/networking.md +0 -316
  199. package/docs/apis/features/node/nlp.md +0 -133
  200. package/docs/apis/features/node/opener.md +0 -97
  201. package/docs/apis/features/node/os.md +0 -146
  202. package/docs/apis/features/node/package-finder.md +0 -392
  203. package/docs/apis/features/node/postgres.md +0 -234
  204. package/docs/apis/features/node/proc.md +0 -399
  205. package/docs/apis/features/node/process-manager.md +0 -305
  206. package/docs/apis/features/node/python.md +0 -604
  207. package/docs/apis/features/node/redis.md +0 -380
  208. package/docs/apis/features/node/repl.md +0 -88
  209. package/docs/apis/features/node/runpod.md +0 -674
  210. package/docs/apis/features/node/secure-shell.md +0 -176
  211. package/docs/apis/features/node/semantic-search.md +0 -408
  212. package/docs/apis/features/node/sqlite.md +0 -233
  213. package/docs/apis/features/node/telegram.md +0 -279
  214. package/docs/apis/features/node/transpiler.md +0 -74
  215. package/docs/apis/features/node/tts.md +0 -133
  216. package/docs/apis/features/node/ui.md +0 -701
  217. package/docs/apis/features/node/vault.md +0 -59
  218. package/docs/apis/features/node/vm.md +0 -75
  219. package/docs/apis/features/node/yaml-tree.md +0 -85
  220. package/docs/apis/features/node/yaml.md +0 -176
  221. package/docs/apis/features/web/asset-loader.md +0 -59
  222. package/docs/apis/features/web/container-link.md +0 -192
  223. package/docs/apis/features/web/esbuild.md +0 -54
  224. package/docs/apis/features/web/helpers.md +0 -164
  225. package/docs/apis/features/web/network.md +0 -44
  226. package/docs/apis/features/web/speech.md +0 -69
  227. package/docs/apis/features/web/vault.md +0 -59
  228. package/docs/apis/features/web/vm.md +0 -75
  229. package/docs/apis/features/web/voice.md +0 -84
  230. package/docs/apis/servers/express.md +0 -171
  231. package/docs/apis/servers/mcp.md +0 -238
  232. package/docs/apis/servers/websocket.md +0 -170
  233. package/docs/bootstrap/CLAUDE.md +0 -101
  234. package/docs/bootstrap/SKILL.md +0 -341
  235. package/docs/bootstrap/templates/about-command.ts +0 -41
  236. package/docs/bootstrap/templates/docs-models.ts +0 -22
  237. package/docs/bootstrap/templates/docs-readme.md +0 -43
  238. package/docs/bootstrap/templates/example-feature.ts +0 -53
  239. package/docs/bootstrap/templates/health-endpoint.ts +0 -15
  240. package/docs/bootstrap/templates/luca-cli.ts +0 -30
  241. package/docs/bootstrap/templates/runme.md +0 -54
  242. package/docs/challenges/caching-proxy.md +0 -16
  243. package/docs/challenges/content-db-round-trip.md +0 -14
  244. package/docs/challenges/custom-command.md +0 -9
  245. package/docs/challenges/file-watcher-pipeline.md +0 -11
  246. package/docs/challenges/grep-audit-report.md +0 -15
  247. package/docs/challenges/multi-feature-dashboard.md +0 -14
  248. package/docs/challenges/process-orchestrator.md +0 -17
  249. package/docs/challenges/rest-api-server-with-client.md +0 -12
  250. package/docs/challenges/script-runner-with-vm.md +0 -11
  251. package/docs/challenges/simple-rest-api.md +0 -15
  252. package/docs/challenges/websocket-serve-and-client.md +0 -11
  253. package/docs/challenges/yaml-config-system.md +0 -14
  254. package/docs/command-system-overhaul.md +0 -94
  255. package/docs/documentation-audit.md +0 -134
  256. package/docs/examples/assistant/CORE.md +0 -18
  257. package/docs/examples/assistant/hooks.ts +0 -3
  258. package/docs/examples/assistant/tools.ts +0 -10
  259. package/docs/examples/assistant-hooks-reference.ts +0 -171
  260. package/docs/examples/assistant-with-process-manager.md +0 -84
  261. package/docs/examples/content-db.md +0 -77
  262. package/docs/examples/disk-cache.md +0 -83
  263. package/docs/examples/docker.md +0 -101
  264. package/docs/examples/downloader.md +0 -70
  265. package/docs/examples/entity.md +0 -124
  266. package/docs/examples/esbuild.md +0 -80
  267. package/docs/examples/feature-as-tool-provider.md +0 -143
  268. package/docs/examples/file-manager.md +0 -82
  269. package/docs/examples/fs.md +0 -83
  270. package/docs/examples/git.md +0 -85
  271. package/docs/examples/google-auth.md +0 -88
  272. package/docs/examples/google-calendar.md +0 -94
  273. package/docs/examples/google-docs.md +0 -82
  274. package/docs/examples/google-drive.md +0 -96
  275. package/docs/examples/google-sheets.md +0 -95
  276. package/docs/examples/grep.md +0 -85
  277. package/docs/examples/ink-blocks.md +0 -75
  278. package/docs/examples/ink-renderer.md +0 -41
  279. package/docs/examples/ink.md +0 -103
  280. package/docs/examples/ipc-socket.md +0 -103
  281. package/docs/examples/json-tree.md +0 -91
  282. package/docs/examples/networking.md +0 -58
  283. package/docs/examples/nlp.md +0 -91
  284. package/docs/examples/opener.md +0 -78
  285. package/docs/examples/os.md +0 -72
  286. package/docs/examples/package-finder.md +0 -89
  287. package/docs/examples/postgres.md +0 -91
  288. package/docs/examples/proc.md +0 -81
  289. package/docs/examples/process-manager.md +0 -79
  290. package/docs/examples/python.md +0 -132
  291. package/docs/examples/repl.md +0 -93
  292. package/docs/examples/runpod.md +0 -119
  293. package/docs/examples/secure-shell.md +0 -92
  294. package/docs/examples/sqlite.md +0 -86
  295. package/docs/examples/structured-output-with-assistants.md +0 -144
  296. package/docs/examples/telegram.md +0 -77
  297. package/docs/examples/tts.md +0 -86
  298. package/docs/examples/ui.md +0 -80
  299. package/docs/examples/vault.md +0 -70
  300. package/docs/examples/vm.md +0 -86
  301. package/docs/examples/websocket-ask-and-reply-example.md +0 -128
  302. package/docs/examples/yaml-tree.md +0 -93
  303. package/docs/examples/yaml.md +0 -104
  304. package/docs/ideas/assistant-factory-pattern.md +0 -142
  305. package/docs/in-memory-fs.md +0 -4
  306. package/docs/introspection-audit.md +0 -49
  307. package/docs/introspection.md +0 -164
  308. package/docs/mcp/readme.md +0 -162
  309. package/docs/models.ts +0 -41
  310. package/docs/philosophy.md +0 -86
  311. package/docs/principles.md +0 -7
  312. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +0 -34
  313. package/docs/prompts/check-for-undocumented-features.md +0 -27
  314. package/docs/prompts/mcp-test-easy-command.md +0 -27
  315. package/docs/scaffolds/client.md +0 -149
  316. package/docs/scaffolds/command.md +0 -120
  317. package/docs/scaffolds/endpoint.md +0 -171
  318. package/docs/scaffolds/feature.md +0 -158
  319. package/docs/scaffolds/selector.md +0 -91
  320. package/docs/scaffolds/server.md +0 -196
  321. package/docs/selectors.md +0 -115
  322. package/docs/sessions/custom-command/attempt-log-2.md +0 -195
  323. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +0 -728
  324. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +0 -555
  325. package/docs/sessions/grep-audit-report/attempt-log-1.md +0 -289
  326. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +0 -679
  327. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +0 -1
  328. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +0 -920
  329. package/docs/sessions/simple-rest-api/attempt-log-1.md +0 -593
  330. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +0 -995
  331. package/docs/tutorials/00-bootstrap.md +0 -166
  332. package/docs/tutorials/01-getting-started.md +0 -106
  333. package/docs/tutorials/02-container.md +0 -210
  334. package/docs/tutorials/03-scripts.md +0 -194
  335. package/docs/tutorials/04-features-overview.md +0 -196
  336. package/docs/tutorials/05-state-and-events.md +0 -171
  337. package/docs/tutorials/06-servers.md +0 -157
  338. package/docs/tutorials/07-endpoints.md +0 -198
  339. package/docs/tutorials/08-commands.md +0 -252
  340. package/docs/tutorials/09-clients.md +0 -162
  341. package/docs/tutorials/10-creating-features.md +0 -203
  342. package/docs/tutorials/11-contentbase.md +0 -191
  343. package/docs/tutorials/12-assistants.md +0 -215
  344. package/docs/tutorials/13-introspection.md +0 -157
  345. package/docs/tutorials/14-type-system.md +0 -174
  346. package/docs/tutorials/15-project-patterns.md +0 -222
  347. package/docs/tutorials/16-google-features.md +0 -534
  348. package/docs/tutorials/17-tui-blocks.md +0 -530
  349. package/docs/tutorials/18-semantic-search.md +0 -334
  350. package/docs/tutorials/19-python-sessions.md +0 -401
  351. package/docs/tutorials/20-browser-esm.md +0 -234
  352. package/index.ts +0 -1
  353. package/src/agi/endpoints/ask.ts +0 -60
  354. package/src/agi/endpoints/conversations/[id].ts +0 -45
  355. package/src/agi/endpoints/conversations.ts +0 -31
  356. package/src/agi/endpoints/experts.ts +0 -37
  357. package/test/assistant-hooks.test.ts +0 -306
  358. package/test/assistant.test.ts +0 -81
  359. package/test/bus.test.ts +0 -134
  360. package/test/clients-servers.test.ts +0 -217
  361. package/test/command.test.ts +0 -267
  362. package/test/container-link.test.ts +0 -274
  363. package/test/conversation.test.ts +0 -220
  364. package/test/features.test.ts +0 -160
  365. package/test/fork-and-research.test.ts +0 -450
  366. package/test/integration.test.ts +0 -787
  367. package/test/interceptor-chain.test.ts +0 -61
  368. package/test/node-container.test.ts +0 -121
  369. package/test/python-session.test.ts +0 -105
  370. package/test/rate-limit.test.ts +0 -272
  371. package/test/semantic-search.test.ts +0 -550
  372. package/test/state.test.ts +0 -121
  373. package/test/vm-context.test.ts +0 -146
  374. package/test/vm-loadmodule.test.ts +0 -213
  375. package/test/websocket-ask.test.ts +0 -101
  376. package/test-integration/assistant.test.ts +0 -138
  377. package/test-integration/assistants-manager.test.ts +0 -113
  378. package/test-integration/claude-code.test.ts +0 -98
  379. package/test-integration/conversation-history.test.ts +0 -205
  380. package/test-integration/conversation.test.ts +0 -137
  381. package/test-integration/elevenlabs.test.ts +0 -55
  382. package/test-integration/google-services.test.ts +0 -80
  383. package/test-integration/helpers.ts +0 -89
  384. package/test-integration/memory.test.ts +0 -204
  385. package/test-integration/openai-codex.test.ts +0 -93
  386. package/test-integration/runpod.test.ts +0 -58
  387. package/test-integration/server-endpoints.test.ts +0 -97
  388. package/test-integration/telegram.test.ts +0 -46
@@ -1,10 +1,10 @@
1
1
  // @ts-nocheck
2
2
  import { z } from 'zod'
3
3
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
4
- import { type AvailableFeatures } from '@soederpop/luca/feature'
4
+ import { type AvailableFeatures } from 'luca/feature'
5
5
  import { Feature } from '../feature.js'
6
6
 
7
- declare module '@soederpop/luca/feature' {
7
+ declare module 'luca/feature' {
8
8
  interface AvailableFeatures {
9
9
  claudeCode: typeof ClaudeCode
10
10
  }
@@ -184,6 +184,12 @@ export const ClaudeCodeOptionsSchema = FeatureOptionsSchema.extend({
184
184
  skillsFolders: z.array(z.string()).optional().describe('Directories containing Claude Code skills to load into sessions'),
185
185
  /** Launch Claude Code with a Chrome browser tool. */
186
186
  chrome: z.boolean().optional().describe('Launch Claude Code with a Chrome browser tool'),
187
+ /** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL env var. */
188
+ baseURL: z.string().optional().describe('Base URL for the Anthropic API, injected as ANTHROPIC_BASE_URL'),
189
+ /** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN env var. */
190
+ authToken: z.string().optional().describe('Auth token for the Anthropic API, injected as ANTHROPIC_AUTH_TOKEN'),
191
+ /** Use local models. Sets baseURL and model from LOCAL_CHAT_ENDPOINT and LOCAL_CODER_MODEL env vars. */
192
+ local: z.boolean().optional().describe('Use local models, sets baseURL to LOCAL_CHAT_ENDPOINT and model to LOCAL_CODER_MODEL'),
187
193
  })
188
194
 
189
195
  export const ClaudeCodeEventsSchema = FeatureEventsSchema.extend({
@@ -269,6 +275,12 @@ export interface RunOptions {
269
275
  settingsFile?: string
270
276
  /** Launch Claude Code with a Chrome browser tool. */
271
277
  chrome?: boolean
278
+ /** Base URL for the Anthropic API. Injected as ANTHROPIC_BASE_URL in the subprocess env. */
279
+ baseURL?: string
280
+ /** Auth token for the Anthropic API. Injected as ANTHROPIC_AUTH_TOKEN in the subprocess env. */
281
+ authToken?: string
282
+ /** Use local models. Sets baseURL to LOCAL_CHAT_ENDPOINT (or http://localhost:1234) and model to LOCAL_CODER_MODEL (or qwen/qwen3.6-27b). */
283
+ local?: boolean
272
284
  }
273
285
 
274
286
  /**
@@ -506,7 +518,8 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
506
518
  args.push('--include-partial-messages')
507
519
  }
508
520
 
509
- const model = options.model ?? this.options.model
521
+ const isLocal = options.local ?? this.options.local
522
+ const model = options.model ?? this.options.model ?? (isLocal ? (process.env.LOCAL_CODER_MODEL || 'qwen/qwen3.6-27b') : undefined)
510
523
  if (model) args.push('--model', model)
511
524
 
512
525
  const systemPrompt = options.systemPrompt ?? this.options.systemPrompt
@@ -613,6 +626,39 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
613
626
  return args
614
627
  }
615
628
 
629
+ /**
630
+ * Build the environment object for a claude CLI invocation.
631
+ * Injects ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN when baseURL/authToken are set,
632
+ * or when local mode is enabled.
633
+ *
634
+ * @param {RunOptions} options - Session options
635
+ * @returns {Record<string, string>} Environment variables
636
+ */
637
+ private buildEnv(options: RunOptions = {}): Record<string, string> {
638
+ const env = { ...process.env }
639
+ const isLocal = options.local ?? this.options.local
640
+
641
+ if (isLocal) {
642
+ const baseURL = process.env.LOCAL_CHAT_ENDPOINT || 'http://localhost:1234'
643
+ env.ANTHROPIC_BASE_URL = baseURL
644
+ if (!options.authToken) {
645
+ env.ANTHROPIC_AUTH_TOKEN = process.env.LOCAL_CHAT_AUTH_TOKEN || 'sk-anticropic-00000000000000000000001'
646
+ }
647
+ }
648
+
649
+ const baseURL = options.baseURL
650
+ if (baseURL) {
651
+ env.ANTHROPIC_BASE_URL = baseURL
652
+ }
653
+
654
+ const authToken = options.authToken
655
+ if (authToken) {
656
+ env.ANTHROPIC_AUTH_TOKEN = authToken
657
+ }
658
+
659
+ return env
660
+ }
661
+
616
662
  /**
617
663
  * Create a unique session ID.
618
664
  *
@@ -780,7 +826,7 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
780
826
  stdout: 'pipe',
781
827
  stderr: 'pipe',
782
828
  stdin: Buffer.from(prompt),
783
- environment: { ...process.env },
829
+ environment: this.buildEnv(options),
784
830
  })
785
831
 
786
832
  this.updateSession(id, { process: proc })
@@ -842,7 +888,7 @@ export class ClaudeCode extends Feature<ClaudeCodeState, ClaudeCodeOptions> {
842
888
  stdout: 'pipe',
843
889
  stderr: 'pipe',
844
890
  stdin: Buffer.from(prompt),
845
- environment: { ...process.env },
891
+ environment: this.buildEnv(options),
846
892
  })
847
893
 
848
894
  this.updateSession(id, { process: proc })
@@ -4,7 +4,7 @@ import { Feature } from '../feature.js'
4
4
  import type { Helper } from '../../helper.js'
5
5
  import type { ChildProcess } from '../../node/features/proc.js'
6
6
 
7
- declare module '@soederpop/luca/feature' {
7
+ declare module 'luca/feature' {
8
8
  interface AvailableFeatures {
9
9
  codingTools: typeof CodingTools
10
10
  }
@@ -1,16 +1,31 @@
1
1
  import { z } from 'zod'
2
2
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
- import { type AvailableFeatures } from '@soederpop/luca/feature'
3
+ import { type AvailableFeatures } from 'luca/feature'
4
4
  import { Feature } from '../feature.js'
5
- import type { DiskCache } from '@soederpop/luca/node/container'
5
+ import type { DiskCache } from 'luca/node/container'
6
+ import type { OpenAIClient } from '../../clients/openai'
6
7
  import type { Message } from './conversation'
7
8
 
8
- declare module '@soederpop/luca/feature' {
9
+ declare module 'luca/feature' {
9
10
  interface AvailableFeatures {
10
11
  conversationHistory: typeof ConversationHistory
11
12
  }
12
13
  }
13
14
 
15
+ export interface TokenUsage {
16
+ prompt: number
17
+ completion: number
18
+ total: number
19
+ cachedTokens?: number
20
+ reasoningTokens?: number
21
+ }
22
+
23
+ export interface CostInfo {
24
+ inputCost: number
25
+ outputCost: number
26
+ totalCost: number
27
+ }
28
+
14
29
  export interface ConversationRecord {
15
30
  id: string
16
31
  title: string
@@ -21,6 +36,8 @@ export interface ConversationRecord {
21
36
  createdAt: string
22
37
  updatedAt: string
23
38
  messageCount: number
39
+ tokenUsage?: TokenUsage
40
+ cost?: CostInfo
24
41
  metadata: Record<string, any>
25
42
  }
26
43
 
@@ -160,12 +177,15 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
160
177
  messages: Message[]
161
178
  tags?: string[]
162
179
  thread?: string
180
+ tokenUsage?: TokenUsage
181
+ cost?: CostInfo
163
182
  metadata?: Record<string, any>
164
183
  }): Promise<ConversationRecord> {
165
184
  const now = new Date().toISOString()
185
+ const title = opts.title || await this.autoTitle(opts.messages)
166
186
  const record: ConversationRecord = {
167
187
  id: opts.id || crypto.randomUUID(),
168
- title: opts.title || 'Untitled',
188
+ title,
169
189
  model: opts.model || 'unknown',
170
190
  messages: opts.messages,
171
191
  tags: opts.tags || [],
@@ -173,6 +193,8 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
173
193
  createdAt: now,
174
194
  updatedAt: now,
175
195
  messageCount: opts.messages.length,
196
+ tokenUsage: opts.tokenUsage,
197
+ cost: opts.cost,
176
198
  metadata: opts.metadata || {},
177
199
  }
178
200
 
@@ -408,6 +430,161 @@ export class ConversationHistory extends Feature<ConversationHistoryState, Conve
408
430
  return count
409
431
  }
410
432
 
433
+ /** @returns An OpenAI client from the container for LLM calls. */
434
+ private get openai(): OpenAIClient {
435
+ return (this.container as any).client('openai') as OpenAIClient
436
+ }
437
+
438
+ /**
439
+ * Generate a short title from conversation messages. Uses the first user
440
+ * message (and optionally the first assistant reply) to produce a concise
441
+ * title via a cheap LLM call. Falls back to a truncated first message
442
+ * if the LLM call fails.
443
+ */
444
+ private async autoTitle(messages: Message[]): Promise<string> {
445
+ const userMsg = messages.find(m => m.role === 'user')
446
+ if (!userMsg) return `Conversation ${new Date().toISOString().slice(0, 16)}`
447
+
448
+ const userContent = typeof userMsg.content === 'string'
449
+ ? userMsg.content
450
+ : Array.isArray(userMsg.content)
451
+ ? (userMsg.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join(' ')
452
+ : ''
453
+
454
+ if (!userContent.trim()) return `Conversation ${new Date().toISOString().slice(0, 16)}`
455
+
456
+ // Build a small context snippet: first user message + first assistant reply if available
457
+ const assistantMsg = messages.find(m => m.role === 'assistant')
458
+ let context = `User: ${userContent}`
459
+ if (assistantMsg) {
460
+ const assistantContent = typeof assistantMsg.content === 'string'
461
+ ? assistantMsg.content
462
+ : Array.isArray(assistantMsg.content)
463
+ ? (assistantMsg.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join(' ')
464
+ : ''
465
+ if (assistantContent.trim()) {
466
+ context += `\nAssistant: ${assistantContent.slice(0, 300)}`
467
+ }
468
+ }
469
+
470
+ try {
471
+ const response = await this.openai.raw.chat.completions.create({
472
+ model: 'gpt-4o-mini',
473
+ messages: [
474
+ {
475
+ role: 'system',
476
+ content: 'Generate a short title (max 8 words) for this conversation. Output only the title, no quotes or punctuation wrapping.',
477
+ },
478
+ { role: 'user', content: context.slice(0, 1000) },
479
+ ],
480
+ max_tokens: 30,
481
+ temperature: 0.3,
482
+ stream: false,
483
+ })
484
+
485
+ const title = (response as any).choices?.[0]?.message?.content?.trim()
486
+ if (title) return title
487
+ } catch {
488
+ // LLM unavailable — fall back to truncation
489
+ }
490
+
491
+ // Fallback: truncate the first user message
492
+ return userContent.length > 60 ? userContent.slice(0, 57) + '...' : userContent
493
+ }
494
+
495
+ /**
496
+ * Build a plain-text transcript from a messages array,
497
+ * suitable for feeding to a summarizer or title generator.
498
+ */
499
+ private buildTranscript(messages: Message[]): string {
500
+ return messages
501
+ .map(m => {
502
+ const role = m.role
503
+ const content = typeof m.content === 'string'
504
+ ? m.content
505
+ : Array.isArray(m.content)
506
+ ? (m.content as any[]).filter((p: any) => p.type === 'text').map((p: any) => p.text).join('\n')
507
+ : (m.content != null ? JSON.stringify(m.content) : '(no content)')
508
+ return `[${role}]: ${content || '(no text content)'}`
509
+ })
510
+ .join('\n\n')
511
+ }
512
+
513
+ /**
514
+ * Generate a concise summary of a stored conversation using the LLM.
515
+ * The summary is stored in `metadata.summary` and returned.
516
+ * No tool calls are made — this is a single completion request.
517
+ *
518
+ * @param {string} id - The conversation ID to summarize
519
+ * @param {object} [options] - Optional settings
520
+ * @param {string} [options.model] - Override the model used for summarization
521
+ * @returns {Promise<string | null>} The generated summary, or null if conversation not found
522
+ */
523
+ async summarize(id: string, options?: { model?: string }): Promise<string | null> {
524
+ const record = await this.load(id)
525
+ if (!record) return null
526
+
527
+ const transcript = this.buildTranscript(record.messages)
528
+ const model = options?.model || record.model || 'gpt-5'
529
+
530
+ const response = await this.openai.raw.chat.completions.create({
531
+ model,
532
+ messages: [
533
+ {
534
+ role: 'system',
535
+ content: 'You are a conversation summarizer. Produce a concise but comprehensive summary of the following conversation. Preserve all key facts, decisions, context, user preferences, and any important details needed to understand what was discussed. Output only the summary.',
536
+ },
537
+ { role: 'user', content: transcript },
538
+ ],
539
+ stream: false,
540
+ })
541
+
542
+ const summary = (response as any).choices?.[0]?.message?.content || ''
543
+
544
+ record.metadata = { ...record.metadata, summary }
545
+ await this.save(record)
546
+
547
+ return summary
548
+ }
549
+
550
+ /**
551
+ * Generate a short, descriptive title for a stored conversation using the LLM.
552
+ * The title is stored both as the record's `title` field and in `metadata.generatedTitle`,
553
+ * then returned. No tool calls are made.
554
+ *
555
+ * @param {string} id - The conversation ID to generate a title for
556
+ * @param {object} [options] - Optional settings
557
+ * @param {string} [options.model] - Override the model used for title generation
558
+ * @returns {Promise<string | null>} The generated title, or null if conversation not found
559
+ */
560
+ async generateTitle(id: string, options?: { model?: string }): Promise<string | null> {
561
+ const record = await this.load(id)
562
+ if (!record) return null
563
+
564
+ const transcript = this.buildTranscript(record.messages)
565
+ const model = options?.model || record.model || 'gpt-5'
566
+
567
+ const response = await this.openai.raw.chat.completions.create({
568
+ model,
569
+ messages: [
570
+ {
571
+ role: 'system',
572
+ content: 'Generate a short, descriptive title (under 60 characters) for the following conversation. The title should capture the main topic or purpose. Output only the title text, with no quotes or punctuation wrapping it.',
573
+ },
574
+ { role: 'user', content: transcript },
575
+ ],
576
+ stream: false,
577
+ })
578
+
579
+ const title = ((response as any).choices?.[0]?.message?.content || '').trim()
580
+
581
+ record.title = title
582
+ record.metadata = { ...record.metadata, generatedTitle: title }
583
+ await this.save(record)
584
+
585
+ return title
586
+ }
587
+
411
588
  // -- index management --
412
589
 
413
590
  private async getIndex(): Promise<string[]> {
@@ -1,13 +1,13 @@
1
1
  import { z } from 'zod'
2
2
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
- import { type AvailableFeatures } from '@soederpop/luca/feature'
3
+ import { type AvailableFeatures } from 'luca/feature'
4
4
  import { Feature } from '../feature.js'
5
5
  import type { OpenAIClient } from '../../clients/openai';
6
6
  import type OpenAI from 'openai';
7
7
  import type { ConversationHistory } from './conversation-history';
8
- import { countMessageTokens, getContextWindow } from '../lib/token-counter.js';
8
+ import { countMessageTokens, getContextWindow, calculateCost } from '../lib/token-counter.js';
9
9
 
10
- declare module '@soederpop/luca/feature' {
10
+ declare module 'luca/feature' {
11
11
  interface AvailableFeatures {
12
12
  conversation: typeof Conversation
13
13
  }
@@ -37,6 +37,20 @@ export interface ConversationMCPServer {
37
37
  }
38
38
  }
39
39
 
40
+ const INPUT_TOKEN_SIZES: Record<string, number> = {
41
+ tiny: 8_000,
42
+ small: 16_000,
43
+ medium: 32_000,
44
+ large: 64_000,
45
+ xlarge: 256_000,
46
+ }
47
+
48
+ function resolveMaxInputTokens(value: number | string | undefined): number | undefined {
49
+ if (value == null) return undefined
50
+ if (typeof value === 'number') return value
51
+ return INPUT_TOKEN_SIZES[value]
52
+ }
53
+
40
54
  export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
41
55
  /** A unique identifier for the conversation */
42
56
  id: z.string().optional().describe('A unique identifier for the conversation'),
@@ -87,6 +101,12 @@ export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
87
101
  contextWindow: z.number().optional().describe('Override the inferred context window size for this model'),
88
102
  /** Number of recent messages to preserve after compaction (default 4) */
89
103
  compactKeepRecent: z.number().optional().describe('Number of recent messages to preserve after compaction (default 4)'),
104
+
105
+ /** Maximum input tokens to send to the API. When set, older messages are trimmed to stay within this budget, keeping the system prompt and most recent messages. Useful for avoiding long-context pricing tiers. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k — max before long-context pricing). */
106
+ maxInputTokens: z.union([
107
+ z.number(),
108
+ z.enum(['tiny', 'small', 'medium', 'large', 'xlarge']),
109
+ ]).default('large').describe('Maximum input tokens. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k). Defaults to large (64k)'),
90
110
  })
91
111
 
92
112
  export const ConversationStateSchema = FeatureStateSchema.extend({
@@ -103,7 +123,14 @@ export const ConversationStateSchema = FeatureStateSchema.extend({
103
123
  prompt: z.number().describe('Total prompt tokens consumed'),
104
124
  completion: z.number().describe('Total completion tokens consumed'),
105
125
  total: z.number().describe('Total tokens consumed'),
106
- }).describe('Cumulative token usage statistics'),
126
+ cachedTokens: z.number().describe('Input tokens served from cache (billed at reduced rate)'),
127
+ reasoningTokens: z.number().describe('Output tokens used for reasoning (o-series models)'),
128
+ }).describe('Cumulative token usage statistics including detail breakdowns from the API'),
129
+ cost: z.object({
130
+ inputCost: z.number().describe('Estimated cost in dollars for input tokens'),
131
+ outputCost: z.number().describe('Estimated cost in dollars for output tokens'),
132
+ totalCost: z.number().describe('Estimated total cost in dollars'),
133
+ }).describe('Running cost estimate based on cumulative token usage and model pricing'),
107
134
  estimatedInputTokens: z.number().describe('Estimated input token count for the current messages array'),
108
135
  compactionCount: z.number().describe('Number of times compact() has been called'),
109
136
  contextWindow: z.number().describe('The context window size for the current model'),
@@ -267,7 +294,8 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
267
294
  toolCalls: 0,
268
295
  api: this.apiMode,
269
296
  lastResponseId: null,
270
- tokenUsage: { prompt: 0, completion: 0, total: 0 },
297
+ tokenUsage: { prompt: 0, completion: 0, total: 0, cachedTokens: 0, reasoningTokens: 0 },
298
+ cost: { inputCost: 0, outputCost: 0, totalCost: 0 },
271
299
  estimatedInputTokens: 0,
272
300
  compactionCount: 0,
273
301
  contextWindow: this.options.contextWindow || getContextWindow(this.options.model || 'gpt-5'),
@@ -598,7 +626,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
598
626
  ? messages[0]
599
627
  : null
600
628
 
601
- const recentMessages = messages.slice(-keepRecent)
629
+ let sliceStart = messages.length - keepRecent
630
+ // Walk back to avoid splitting a tool call group — if we'd start on a tool message,
631
+ // include the preceding assistant message (and its full tool response block)
632
+ if (sliceStart > 0) {
633
+ while (sliceStart > 0 && messages[sliceStart]?.role === 'tool') {
634
+ sliceStart--
635
+ }
636
+ }
637
+ const recentMessages = messages.slice(sliceStart)
602
638
 
603
639
  const newMessages: Message[] = []
604
640
  if (systemMessage) newMessages.push(systemMessage)
@@ -724,16 +760,20 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
724
760
  let raw: string
725
761
 
726
762
  if (this.apiMode === 'responses') {
727
- const previousResponseId = this.state.get('lastResponseId') || undefined
763
+ // When maxInputTokens is set, skip previous_response_id continuation
764
+ // so we control exactly how many tokens the API processes (server-side
765
+ // context from previous_response_id would accumulate unbounded).
766
+ const canChain = !this.options.maxInputTokens
767
+ const previousResponseId = canChain ? (this.state.get('lastResponseId') || undefined) : undefined
728
768
  let input: OpenAI.Responses.ResponseInput
729
769
 
730
770
  if (previousResponseId) {
731
771
  // Can chain via previous_response_id — only send the new user message
732
772
  input = [this.toResponsesUserMessage(content)]
733
773
  } else {
734
- // No previous response ID (first call or resumed from disk).
735
- // Convert full message history to Responses API input so the model has context.
736
- input = this.messagesToResponsesInput()
774
+ // No previous response ID (first call, resumed from disk, or maxInputTokens active).
775
+ // Convert (possibly trimmed) message history to Responses API input.
776
+ input = this.messagesToResponsesInput(this.getMessagesWithinBudget())
737
777
  }
738
778
 
739
779
  raw = await this.runResponsesLoop({
@@ -816,10 +856,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
816
856
  * Convert the full Chat Completions message history into Responses API input items.
817
857
  * Used when resuming a conversation without a previous_response_id.
818
858
  */
819
- private messagesToResponsesInput(): OpenAI.Responses.ResponseInput {
859
+ private messagesToResponsesInput(messages?: Message[]): OpenAI.Responses.ResponseInput {
820
860
  const input: OpenAI.Responses.ResponseInput = []
821
861
 
822
- for (const msg of this.messages) {
862
+ for (const msg of (messages || this.messages)) {
823
863
  if (msg.role === 'system' || msg.role === 'developer') {
824
864
  // System/developer messages are handled via the instructions parameter
825
865
  continue
@@ -917,9 +957,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
917
957
  const lastResponseId = this.state.get('lastResponseId')
918
958
  const responseMeta = lastResponseId ? { lastResponseId } : {}
919
959
 
960
+ // Grab the live token usage and cost from state
961
+ const tokenUsage = this.state.get('tokenUsage')!
962
+ const cost = this.state.get('cost')!
963
+
920
964
  if (existing) {
921
965
  existing.messages = this.messages
922
966
  existing.model = this.model
967
+ existing.tokenUsage = tokenUsage
968
+ existing.cost = cost
923
969
  if (opts?.title) existing.title = opts.title
924
970
  if (opts?.tags) existing.tags = opts.tags
925
971
  if (opts?.thread) existing.thread = opts.thread
@@ -930,11 +976,13 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
930
976
 
931
977
  return this.history.create({
932
978
  id,
933
- title: opts?.title || this.options.title || 'Untitled',
979
+ title: opts?.title || this.options.title,
934
980
  model: this.model,
935
981
  messages: this.messages,
936
982
  tags: opts?.tags || this.options.tags || [],
937
983
  thread: opts?.thread || this.options.thread || this.state.get('thread'),
984
+ tokenUsage,
985
+ cost,
938
986
  metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
939
987
  })
940
988
  }
@@ -1163,6 +1211,16 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
1163
1211
  return accumulated || finalText
1164
1212
  }
1165
1213
 
1214
+ /** Recalculate the running cost estimate from current token usage and update state. */
1215
+ private updateCost() {
1216
+ const tokenUsage = this.state.get('tokenUsage')!
1217
+ const { inputCost, outputCost, totalCost } = calculateCost(this.model, tokenUsage.prompt, tokenUsage.completion, {
1218
+ cachedTokens: tokenUsage.cachedTokens,
1219
+ reasoningTokens: tokenUsage.reasoningTokens,
1220
+ })
1221
+ this.state.set('cost', { inputCost, outputCost, totalCost })
1222
+ }
1223
+
1166
1224
  /** Apply Responses API usage stats to this conversation's token usage counters. */
1167
1225
  private applyResponsesUsage(usage?: OpenAI.Responses.ResponseUsage) {
1168
1226
  if (!usage) return
@@ -1171,7 +1229,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
1171
1229
  prompt: prev.prompt + (usage.input_tokens || 0),
1172
1230
  completion: prev.completion + (usage.output_tokens || 0),
1173
1231
  total: prev.total + (usage.total_tokens || 0),
1232
+ cachedTokens: prev.cachedTokens + (usage.input_tokens_details?.cached_tokens || 0),
1233
+ reasoningTokens: prev.reasoningTokens + (usage.output_tokens_details?.reasoning_tokens || 0),
1174
1234
  })
1235
+ this.updateCost()
1175
1236
  }
1176
1237
 
1177
1238
  /**
@@ -1208,7 +1269,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
1208
1269
  try {
1209
1270
  const stream = await this.openai.raw.chat.completions.create({
1210
1271
  model: this.model,
1211
- messages: this.messages,
1272
+ messages: this.sanitizeMessages(this.getMessagesWithinBudget()),
1212
1273
  stream: true,
1213
1274
  ...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
1214
1275
  ...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
@@ -1258,8 +1319,11 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
1258
1319
  this.state.set('tokenUsage', {
1259
1320
  prompt: prev.prompt + (chunk.usage.prompt_tokens || 0),
1260
1321
  completion: prev.completion + (chunk.usage.completion_tokens || 0),
1261
- total: prev.total + (chunk.usage.total_tokens || 0)
1322
+ total: prev.total + (chunk.usage.total_tokens || 0),
1323
+ cachedTokens: prev.cachedTokens + (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
1324
+ reasoningTokens: prev.reasoningTokens + (chunk.usage.completion_tokens_details?.reasoning_tokens || 0),
1262
1325
  })
1326
+ this.updateCost()
1263
1327
  }
1264
1328
  }
1265
1329
  } finally {
@@ -1310,6 +1374,113 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
1310
1374
  return accumulated
1311
1375
  }
1312
1376
 
1377
+ /**
1378
+ * Returns the messages array trimmed to fit within the maxInputTokens budget.
1379
+ * Keeps the system/developer message and drops oldest atomic groups first.
1380
+ *
1381
+ * Messages are grouped into atomic units so tool call/response pairs are never
1382
+ * split (which would cause a 400 from OpenAI):
1383
+ * - assistant with tool_calls + its subsequent tool response messages = one group
1384
+ * - standalone user, assistant (no tools), system = one group each
1385
+ *
1386
+ * If no maxInputTokens is set, returns messages as-is.
1387
+ */
1388
+ private getMessagesWithinBudget(): Message[] {
1389
+ const budget = resolveMaxInputTokens(this.options.maxInputTokens)
1390
+ if (!budget) return this.messages
1391
+
1392
+ const messages = this.messages
1393
+ if (messages.length === 0) return messages
1394
+
1395
+ // Check if the full history already fits
1396
+ const fullCount = countMessageTokens(messages, this.model)
1397
+ if (fullCount <= budget) return messages
1398
+
1399
+ // Separate system prompt from the rest
1400
+ const systemMsg = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
1401
+ ? messages[0]
1402
+ : null
1403
+ const nonSystem = systemMsg ? messages.slice(1) : [...messages]
1404
+
1405
+ // Group messages into atomic units.
1406
+ // An assistant message with tool_calls and its subsequent tool responses form one group.
1407
+ type MessageGroup = Message[]
1408
+ const groups: MessageGroup[] = []
1409
+ let i = 0
1410
+ while (i < nonSystem.length) {
1411
+ const msg = nonSystem[i]!
1412
+ if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
1413
+ // Collect the assistant + all following tool responses that belong to it
1414
+ const expectedIds = new Set(((msg as any).tool_calls as any[]).map((tc: any) => tc.id))
1415
+ const group: Message[] = [msg]
1416
+ let j = i + 1
1417
+ while (j < nonSystem.length && nonSystem[j]!.role === 'tool' && expectedIds.has((nonSystem[j] as any).tool_call_id)) {
1418
+ group.push(nonSystem[j]!)
1419
+ j++
1420
+ }
1421
+ groups.push(group)
1422
+ i = j
1423
+ } else {
1424
+ groups.push([msg])
1425
+ i++
1426
+ }
1427
+ }
1428
+
1429
+ // Walk backwards through groups, accumulating tokens until we exceed the budget
1430
+ const systemTokens = systemMsg ? countMessageTokens([systemMsg], this.model) : 0
1431
+ let running = systemTokens
1432
+ let cutoff = groups.length // start with nothing included
1433
+
1434
+ for (let g = groups.length - 1; g >= 0; g--) {
1435
+ const groupTokens = countMessageTokens(groups[g]!, this.model)
1436
+ if (running + groupTokens > budget) break
1437
+ running += groupTokens
1438
+ cutoff = g
1439
+ }
1440
+
1441
+ const kept = groups.slice(cutoff).flat()
1442
+ return systemMsg ? [systemMsg, ...kept] : kept
1443
+ }
1444
+
1445
+ private sanitizeMessages(messages: Message[]): Message[] {
1446
+ const result: Message[] = []
1447
+
1448
+ for (let i = 0; i < messages.length; i++) {
1449
+ const msg = messages[i]!
1450
+ result.push(msg)
1451
+
1452
+ // Check if this is an assistant message with tool_calls
1453
+ if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
1454
+ const toolCalls: Array<{ id: string }> = (msg as any).tool_calls
1455
+ const expectedIds = new Set(toolCalls.map(tc => tc.id))
1456
+
1457
+ // Scan forward for matching tool responses
1458
+ const foundIds = new Set<string>()
1459
+ for (let j = i + 1; j < messages.length; j++) {
1460
+ const next = messages[j]!
1461
+ if (next.role === 'tool' && expectedIds.has((next as any).tool_call_id)) {
1462
+ foundIds.add((next as any).tool_call_id)
1463
+ } else if (next.role !== 'tool') {
1464
+ break
1465
+ }
1466
+ }
1467
+
1468
+ // Add stub responses for any missing tool_call_ids
1469
+ for (const id of expectedIds) {
1470
+ if (!foundIds.has(id)) {
1471
+ result.push({
1472
+ role: 'tool',
1473
+ tool_call_id: id,
1474
+ content: '[tool execution was interrupted]',
1475
+ } as any)
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ return result
1482
+ }
1483
+
1313
1484
  /**
1314
1485
  * Append a message to the conversation state.
1315
1486
  *
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod'
2
2
  import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
- import { type AvailableFeatures } from '@soederpop/luca/feature'
3
+ import { type AvailableFeatures } from 'luca/feature'
4
4
  import { Feature } from '../feature.js'
5
5
  import type { ContentDb } from '@/node.js'
6
6
  import type Assistant from './assistant.js'
7
7
 
8
- declare module '@soederpop/luca/feature' {
8
+ declare module 'luca/feature' {
9
9
  interface AvailableFeatures {
10
10
  docsReader: typeof DocsReader
11
11
  }