@neurocode-ai/core 1.18.8

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 (490) hide show
  1. package/bin/neurocode +199 -0
  2. package/bunfig.toml +2 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +131 -0
  5. package/schema.json +2071 -0
  6. package/script/fix-node-pty.ts +28 -0
  7. package/script/migration.ts +196 -0
  8. package/src/account/sql.ts +39 -0
  9. package/src/account.ts +101 -0
  10. package/src/agent.ts +111 -0
  11. package/src/aisdk.ts +235 -0
  12. package/src/background-job.ts +365 -0
  13. package/src/catalog.ts +301 -0
  14. package/src/command.ts +64 -0
  15. package/src/config/agent.ts +25 -0
  16. package/src/config/attachments.ts +15 -0
  17. package/src/config/command.ts +12 -0
  18. package/src/config/compaction.ts +15 -0
  19. package/src/config/experimental.ts +18 -0
  20. package/src/config/formatter.ts +12 -0
  21. package/src/config/lsp.ts +18 -0
  22. package/src/config/markdown.ts +36 -0
  23. package/src/config/mcp.ts +48 -0
  24. package/src/config/plugin/agent.ts +179 -0
  25. package/src/config/plugin/command.ts +83 -0
  26. package/src/config/plugin/external.ts +91 -0
  27. package/src/config/plugin/provider.ts +113 -0
  28. package/src/config/plugin/reference.ts +70 -0
  29. package/src/config/plugin/skill.ts +50 -0
  30. package/src/config/plugin.ts +13 -0
  31. package/src/config/provider.ts +71 -0
  32. package/src/config/reference.ts +22 -0
  33. package/src/config/tool-output.ts +9 -0
  34. package/src/config/watcher.ts +7 -0
  35. package/src/config.ts +227 -0
  36. package/src/control-plane/move-session.ts +148 -0
  37. package/src/control-plane/workspace.sql.ts +20 -0
  38. package/src/credential/sql.ts +14 -0
  39. package/src/credential.ts +138 -0
  40. package/src/cross-spawn-spawner.ts +507 -0
  41. package/src/data-migration.sql.ts +6 -0
  42. package/src/database/database.ts +57 -0
  43. package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
  44. package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
  45. package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
  46. package/src/database/migration/20260225215848_workspace.ts +19 -0
  47. package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
  48. package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
  49. package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
  50. package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
  51. package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
  52. package/src/database/migration/20260323234822_events.ts +26 -0
  53. package/src/database/migration/20260410174513_workspace-name.ts +29 -0
  54. package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
  55. package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
  56. package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
  57. package/src/database/migration/20260428004200_add_session_path.ts +11 -0
  58. package/src/database/migration/20260501142318_next_venus.ts +12 -0
  59. package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
  60. package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
  61. package/src/database/migration/20260510033149_session_usage.ts +56 -0
  62. package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
  63. package/src/database/migration/20260511173437_session-metadata.ts +16 -0
  64. package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
  65. package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
  66. package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
  67. package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
  68. package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
  69. package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
  70. package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
  71. package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
  72. package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
  73. package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
  74. package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
  75. package/src/database/migration/20260611035744_credential.ts +25 -0
  76. package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
  77. package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
  78. package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
  79. package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
  80. package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
  81. package/src/database/migration.gen.ts +44 -0
  82. package/src/database/migration.ts +81 -0
  83. package/src/database/path.ts +91 -0
  84. package/src/database/schema.gen.ts +274 -0
  85. package/src/database/schema.sql.ts +10 -0
  86. package/src/database/sqlite.bun.ts +183 -0
  87. package/src/database/sqlite.node.ts +178 -0
  88. package/src/database/sqlite.ts +8 -0
  89. package/src/effect/app-node-builder.ts +23 -0
  90. package/src/effect/app-node-platform.ts +18 -0
  91. package/src/effect/app-node.ts +14 -0
  92. package/src/effect/dfdf +1 -0
  93. package/src/effect/keyed-mutex.ts +45 -0
  94. package/src/effect/layer-node.ts +333 -0
  95. package/src/effect/memo-map.ts +3 -0
  96. package/src/effect/runtime.ts +21 -0
  97. package/src/effect/service-use.ts +43 -0
  98. package/src/event/sql.ts +25 -0
  99. package/src/event.ts +638 -0
  100. package/src/file-mutation.ts +207 -0
  101. package/src/file.ts +6 -0
  102. package/src/filesystem/fff.bun.ts +140 -0
  103. package/src/filesystem/fff.node.ts +138 -0
  104. package/src/filesystem/ignore.ts +67 -0
  105. package/src/filesystem/protected.ts +53 -0
  106. package/src/filesystem/search.ts +239 -0
  107. package/src/filesystem/watcher.ts +140 -0
  108. package/src/filesystem.ts +118 -0
  109. package/src/flag/flag.ts +78 -0
  110. package/src/fs-util.ts +274 -0
  111. package/src/git.ts +987 -0
  112. package/src/github-copilot/README.md +5 -0
  113. package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  114. package/src/github-copilot/chat/get-response-metadata.ts +15 -0
  115. package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  116. package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
  117. package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  118. package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
  119. package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  120. package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  121. package/src/github-copilot/copilot-provider.ts +100 -0
  122. package/src/github-copilot/openai-compatible-error.ts +27 -0
  123. package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
  124. package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  125. package/src/github-copilot/responses/openai-config.ts +18 -0
  126. package/src/github-copilot/responses/openai-error.ts +22 -0
  127. package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
  128. package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
  129. package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
  130. package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
  131. package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
  132. package/src/github-copilot/responses/tool/file-search.ts +127 -0
  133. package/src/github-copilot/responses/tool/image-generation.ts +114 -0
  134. package/src/github-copilot/responses/tool/local-shell.ts +64 -0
  135. package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
  136. package/src/github-copilot/responses/tool/web-search.ts +102 -0
  137. package/src/global.ts +87 -0
  138. package/src/id/id.ts +47 -0
  139. package/src/image/photon.ts +94 -0
  140. package/src/image.ts +81 -0
  141. package/src/installation/version.ts +8 -0
  142. package/src/instruction-context.ts +101 -0
  143. package/src/integration/connection.ts +12 -0
  144. package/src/integration.ts +520 -0
  145. package/src/location-mutation.ts +162 -0
  146. package/src/location-service-map.ts +18 -0
  147. package/src/location-services.ts +115 -0
  148. package/src/location.ts +39 -0
  149. package/src/markdown.d.ts +4 -0
  150. package/src/model.ts +41 -0
  151. package/src/models-dev.ts +260 -0
  152. package/src/npm-config.ts +40 -0
  153. package/src/npm.ts +269 -0
  154. package/src/oauth/page.ts +276 -0
  155. package/src/observability/logging.ts +71 -0
  156. package/src/observability/otlp.ts +79 -0
  157. package/src/observability/shared.ts +1 -0
  158. package/src/observability.ts +24 -0
  159. package/src/patch.ts +197 -0
  160. package/src/permission/saved.ts +79 -0
  161. package/src/permission/sql.ts +20 -0
  162. package/src/permission.ts +310 -0
  163. package/src/plugin/agent.ts +206 -0
  164. package/src/plugin/command/initialize.txt +65 -0
  165. package/src/plugin/command/review.txt +100 -0
  166. package/src/plugin/command.ts +25 -0
  167. package/src/plugin/host.ts +219 -0
  168. package/src/plugin/internal.ts +153 -0
  169. package/src/plugin/layer-map.example.ts +94 -0
  170. package/src/plugin/models-dev.ts +183 -0
  171. package/src/plugin/promise.ts +93 -0
  172. package/src/plugin/provider/alibaba.ts +15 -0
  173. package/src/plugin/provider/amazon-bedrock.ts +126 -0
  174. package/src/plugin/provider/anthropic.ts +27 -0
  175. package/src/plugin/provider/azure.ts +82 -0
  176. package/src/plugin/provider/cerebras.ts +26 -0
  177. package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
  178. package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
  179. package/src/plugin/provider/cohere.ts +15 -0
  180. package/src/plugin/provider/deepinfra.ts +15 -0
  181. package/src/plugin/provider/dynamic.ts +31 -0
  182. package/src/plugin/provider/gateway.ts +15 -0
  183. package/src/plugin/provider/github-copilot.ts +52 -0
  184. package/src/plugin/provider/gitlab.ts +65 -0
  185. package/src/plugin/provider/google-vertex.ts +173 -0
  186. package/src/plugin/provider/google.ts +15 -0
  187. package/src/plugin/provider/groq.ts +15 -0
  188. package/src/plugin/provider/kilo.ts +21 -0
  189. package/src/plugin/provider/llmgateway.ts +26 -0
  190. package/src/plugin/provider/mistral.ts +15 -0
  191. package/src/plugin/provider/nvidia.ts +22 -0
  192. package/src/plugin/provider/openai-compatible.ts +17 -0
  193. package/src/plugin/provider/openai.ts +292 -0
  194. package/src/plugin/provider/opencode.ts +311 -0
  195. package/src/plugin/provider/openrouter.ts +36 -0
  196. package/src/plugin/provider/perplexity.ts +15 -0
  197. package/src/plugin/provider/sap-ai-core.ts +46 -0
  198. package/src/plugin/provider/snowflake-cortex.ts +89 -0
  199. package/src/plugin/provider/togetherai.ts +15 -0
  200. package/src/plugin/provider/venice.ts +15 -0
  201. package/src/plugin/provider/vercel.ts +27 -0
  202. package/src/plugin/provider/xai.ts +22 -0
  203. package/src/plugin/provider/zenmux.ts +21 -0
  204. package/src/plugin/provider.ts +71 -0
  205. package/src/plugin/skill/customize-neurocode.md +452 -0
  206. package/src/plugin/skill.ts +31 -0
  207. package/src/plugin/variant.ts +39 -0
  208. package/src/plugin.ts +167 -0
  209. package/src/policy.ts +49 -0
  210. package/src/process.ts +261 -0
  211. package/src/project/copy-strategies.ts +35 -0
  212. package/src/project/copy.ts +292 -0
  213. package/src/project/directories.ts +158 -0
  214. package/src/project/schema.ts +16 -0
  215. package/src/project/sql.ts +35 -0
  216. package/src/project.ts +136 -0
  217. package/src/provider.ts +25 -0
  218. package/src/pty/protocol.ts +37 -0
  219. package/src/pty/pty.bun.ts +26 -0
  220. package/src/pty/pty.node.ts +29 -0
  221. package/src/pty/pty.ts +25 -0
  222. package/src/pty/schema.ts +1 -0
  223. package/src/pty/ticket.ts +56 -0
  224. package/src/pty.ts +318 -0
  225. package/src/public-event-manifest.ts +7 -0
  226. package/src/question.ts +153 -0
  227. package/src/reference/guidance.ts +69 -0
  228. package/src/reference.ts +125 -0
  229. package/src/repository-cache.ts +259 -0
  230. package/src/repository.ts +214 -0
  231. package/src/ripgrep/binary.ts +132 -0
  232. package/src/ripgrep.ts +281 -0
  233. package/src/schema.ts +79 -0
  234. package/src/session/compaction.ts +241 -0
  235. package/src/session/context-epoch.ts +174 -0
  236. package/src/session/error.ts +24 -0
  237. package/src/session/event.ts +2 -0
  238. package/src/session/execution/local.ts +46 -0
  239. package/src/session/execution.ts +34 -0
  240. package/src/session/history.ts +101 -0
  241. package/src/session/info.ts +50 -0
  242. package/src/session/input.ts +288 -0
  243. package/src/session/message-updater.ts +397 -0
  244. package/src/session/message.ts +2 -0
  245. package/src/session/projector.ts +458 -0
  246. package/src/session/prompt.ts +1 -0
  247. package/src/session/revert.ts +121 -0
  248. package/src/session/run-coordinator.ts +104 -0
  249. package/src/session/runner/index.ts +28 -0
  250. package/src/session/runner/llm.ts +432 -0
  251. package/src/session/runner/max-steps.ts +16 -0
  252. package/src/session/runner/model.ts +218 -0
  253. package/src/session/runner/publish-llm-event.ts +423 -0
  254. package/src/session/runner/to-llm-message.ts +171 -0
  255. package/src/session/schema.ts +9 -0
  256. package/src/session/sql.ts +176 -0
  257. package/src/session/store.ts +63 -0
  258. package/src/session/todo.ts +78 -0
  259. package/src/session.ts +486 -0
  260. package/src/share/sql.ts +13 -0
  261. package/src/shell.ts +226 -0
  262. package/src/skill/discovery.ts +213 -0
  263. package/src/skill/guidance.ts +76 -0
  264. package/src/skill.ts +132 -0
  265. package/src/snapshot.ts +266 -0
  266. package/src/state.ts +128 -0
  267. package/src/system-context/builtins.ts +50 -0
  268. package/src/system-context/index.ts +320 -0
  269. package/src/system-context/registry.ts +49 -0
  270. package/src/tool/AGENTS.md +59 -0
  271. package/src/tool/application-tools.ts +57 -0
  272. package/src/tool/apply-patch.ts +219 -0
  273. package/src/tool/bash.ts +207 -0
  274. package/src/tool/builtins.ts +48 -0
  275. package/src/tool/edit.ts +223 -0
  276. package/src/tool/glob.ts +105 -0
  277. package/src/tool/grep.ts +137 -0
  278. package/src/tool/http-body.ts +30 -0
  279. package/src/tool/question.ts +94 -0
  280. package/src/tool/read-filesystem.ts +366 -0
  281. package/src/tool/read.ts +117 -0
  282. package/src/tool/registry.ts +147 -0
  283. package/src/tool/skill.ts +109 -0
  284. package/src/tool/todowrite.ts +62 -0
  285. package/src/tool/tool.ts +162 -0
  286. package/src/tool/tools.ts +13 -0
  287. package/src/tool/webfetch.ts +218 -0
  288. package/src/tool/websearch.ts +260 -0
  289. package/src/tool/write.ts +101 -0
  290. package/src/tool-output-store.ts +211 -0
  291. package/src/util/array.ts +10 -0
  292. package/src/util/binary.ts +41 -0
  293. package/src/util/effect-flock.ts +284 -0
  294. package/src/util/encode.ts +51 -0
  295. package/src/util/error.ts +70 -0
  296. package/src/util/flock.ts +358 -0
  297. package/src/util/glob.ts +34 -0
  298. package/src/util/hash.ts +11 -0
  299. package/src/util/identifier.ts +1 -0
  300. package/src/util/iife.ts +3 -0
  301. package/src/util/lazy.ts +11 -0
  302. package/src/util/module.ts +10 -0
  303. package/src/util/path.ts +37 -0
  304. package/src/util/retry.ts +42 -0
  305. package/src/util/slug.ts +74 -0
  306. package/src/util/token.ts +5 -0
  307. package/src/util/which.ts +14 -0
  308. package/src/util/wildcard.ts +14 -0
  309. package/src/v1/config/agent.ts +89 -0
  310. package/src/v1/config/attachment.ts +25 -0
  311. package/src/v1/config/command.ts +13 -0
  312. package/src/v1/config/config.ts +192 -0
  313. package/src/v1/config/console-state.ts +16 -0
  314. package/src/v1/config/error.ts +39 -0
  315. package/src/v1/config/formatter.ts +13 -0
  316. package/src/v1/config/layout.ts +6 -0
  317. package/src/v1/config/lsp.ts +80 -0
  318. package/src/v1/config/mcp.ts +63 -0
  319. package/src/v1/config/migrate.ts +254 -0
  320. package/src/v1/config/permission.ts +50 -0
  321. package/src/v1/config/plugin.ts +9 -0
  322. package/src/v1/config/provider-options.ts +227 -0
  323. package/src/v1/config/provider.ts +121 -0
  324. package/src/v1/config/server.ts +19 -0
  325. package/src/v1/config/skills.ts +13 -0
  326. package/src/v1/permission.ts +33 -0
  327. package/src/v1/session.ts +68 -0
  328. package/src/v2-schema.ts +3 -0
  329. package/src/workspace.ts +6 -0
  330. package/sst-env.d.ts +10 -0
  331. package/test/agent.test.ts +131 -0
  332. package/test/application-tools.test.ts +287 -0
  333. package/test/background-job.test.ts +106 -0
  334. package/test/catalog.test.ts +353 -0
  335. package/test/command.test.ts +56 -0
  336. package/test/config/agent.test.ts +349 -0
  337. package/test/config/command.test.ts +83 -0
  338. package/test/config/config.test.ts +798 -0
  339. package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
  340. package/test/config/plugin.test.ts +248 -0
  341. package/test/config/provider-options.test.ts +224 -0
  342. package/test/config/provider.test.ts +269 -0
  343. package/test/config/skill.test.ts +80 -0
  344. package/test/credential.test.ts +36 -0
  345. package/test/database-migration.test.ts +644 -0
  346. package/test/effect/cross-spawn-spawner.test.ts +426 -0
  347. package/test/effect/keyed-mutex.test.ts +73 -0
  348. package/test/effect/layer-node/layer-node-types.test.ts +143 -0
  349. package/test/effect/layer-node/layer-node.test.ts +261 -0
  350. package/test/effect/layer-node/node-build.test.ts +125 -0
  351. package/test/effect/observability.test.ts +109 -0
  352. package/test/event.test.ts +1124 -0
  353. package/test/file-mutation.test.ts +368 -0
  354. package/test/filesystem/filesystem.test.ts +387 -0
  355. package/test/filesystem/ignore.test.ts +10 -0
  356. package/test/filesystem/search.test.ts +44 -0
  357. package/test/filesystem/watcher.test.ts +266 -0
  358. package/test/fixture/effect-flock-worker.ts +60 -0
  359. package/test/fixture/flock-worker.ts +72 -0
  360. package/test/fixture/git.ts +49 -0
  361. package/test/fixture/location.ts +26 -0
  362. package/test/fixture/tmpdir.ts +25 -0
  363. package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
  364. package/test/git.test.ts +164 -0
  365. package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
  366. package/test/github-copilot/copilot-chat-model.test.ts +592 -0
  367. package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
  368. package/test/global.test.ts +16 -0
  369. package/test/instruction-context.test.ts +323 -0
  370. package/test/integration.test.ts +349 -0
  371. package/test/legacy-event-schema.test.ts +16 -0
  372. package/test/lib/effect.ts +53 -0
  373. package/test/lib/tool.ts +20 -0
  374. package/test/location-filesystem.test.ts +69 -0
  375. package/test/location-layer.test.ts +226 -0
  376. package/test/location-mutation.test.ts +178 -0
  377. package/test/location.test.ts +42 -0
  378. package/test/model.test.ts +23 -0
  379. package/test/models.test.ts +290 -0
  380. package/test/move-session.test.ts +235 -0
  381. package/test/npm-config.test.ts +51 -0
  382. package/test/npm.test.ts +84 -0
  383. package/test/oauth-page.test.ts +15 -0
  384. package/test/patch.test.ts +68 -0
  385. package/test/permission.test.ts +315 -0
  386. package/test/plugin/command.test.ts +47 -0
  387. package/test/plugin/fixture.ts +52 -0
  388. package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
  389. package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
  390. package/test/plugin/fixtures/invalid-plugin.ts +1 -0
  391. package/test/plugin/fixtures/models-dev.json +14 -0
  392. package/test/plugin/fixtures/provider-factory.ts +9 -0
  393. package/test/plugin/host.ts +303 -0
  394. package/test/plugin/models-dev.test.ts +170 -0
  395. package/test/plugin/promise.test.ts +67 -0
  396. package/test/plugin/provider-alibaba.test.ts +92 -0
  397. package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
  398. package/test/plugin/provider-anthropic.test.ts +94 -0
  399. package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
  400. package/test/plugin/provider-azure.test.ts +293 -0
  401. package/test/plugin/provider-cerebras.test.ts +136 -0
  402. package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
  403. package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
  404. package/test/plugin/provider-cohere.test.ts +123 -0
  405. package/test/plugin/provider-deepinfra.test.ts +159 -0
  406. package/test/plugin/provider-dynamic.test.ts +184 -0
  407. package/test/plugin/provider-gateway.test.ts +115 -0
  408. package/test/plugin/provider-github-copilot.test.ts +278 -0
  409. package/test/plugin/provider-gitlab.test.ts +308 -0
  410. package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
  411. package/test/plugin/provider-google-vertex.test.ts +387 -0
  412. package/test/plugin/provider-google.test.ts +80 -0
  413. package/test/plugin/provider-groq.test.ts +121 -0
  414. package/test/plugin/provider-kilo.test.ts +99 -0
  415. package/test/plugin/provider-llmgateway.test.ts +80 -0
  416. package/test/plugin/provider-mistral.test.ts +129 -0
  417. package/test/plugin/provider-nvidia.test.ts +97 -0
  418. package/test/plugin/provider-openai-compatible.test.ts +107 -0
  419. package/test/plugin/provider-openai.test.ts +176 -0
  420. package/test/plugin/provider-opencode.test.ts +423 -0
  421. package/test/plugin/provider-openrouter.test.ts +113 -0
  422. package/test/plugin/provider-perplexity.test.ts +122 -0
  423. package/test/plugin/provider-sap-ai-core.test.ts +159 -0
  424. package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
  425. package/test/plugin/provider-togetherai.test.ts +129 -0
  426. package/test/plugin/provider-venice.test.ts +115 -0
  427. package/test/plugin/provider-vercel.test.ts +83 -0
  428. package/test/plugin/provider-xai.test.ts +125 -0
  429. package/test/plugin/provider-zenmux.test.ts +110 -0
  430. package/test/plugin/skill.test.ts +25 -0
  431. package/test/plugin/variant.test.ts +67 -0
  432. package/test/plugin.test.ts +71 -0
  433. package/test/policy.test.ts +85 -0
  434. package/test/preload.ts +5 -0
  435. package/test/process/process.test.ts +368 -0
  436. package/test/project-copy.test.ts +400 -0
  437. package/test/project-directories.test.ts +61 -0
  438. package/test/project.test.ts +221 -0
  439. package/test/provider-mistral.test.ts +282 -0
  440. package/test/provider-xai-responses.test.ts +32 -0
  441. package/test/pty/info-schema.test.ts +32 -0
  442. package/test/pty/protocol.test.ts +27 -0
  443. package/test/pty/pty-session.test.ts +240 -0
  444. package/test/pty/ticket.test.ts +62 -0
  445. package/test/question.test.ts +114 -0
  446. package/test/reference-guidance.test.ts +76 -0
  447. package/test/reference.test.ts +78 -0
  448. package/test/repository-cache.test.ts +153 -0
  449. package/test/repository.test.ts +71 -0
  450. package/test/ripgrep.test.ts +65 -0
  451. package/test/session-compaction.test.ts +27 -0
  452. package/test/session-create.test.ts +425 -0
  453. package/test/session-history.test.ts +165 -0
  454. package/test/session-projector.test.ts +533 -0
  455. package/test/session-prompt.test.ts +584 -0
  456. package/test/session-run-coordinator.test.ts +418 -0
  457. package/test/session-runner-message.test.ts +501 -0
  458. package/test/session-runner-model.test.ts +347 -0
  459. package/test/session-runner-recorded.test.ts +193 -0
  460. package/test/session-runner-tool-events.test.ts +136 -0
  461. package/test/session-runner-tool-registry.test.ts +452 -0
  462. package/test/session-runner.test.ts +3365 -0
  463. package/test/session-todo.test.ts +94 -0
  464. package/test/session-tool-progress.test.ts +158 -0
  465. package/test/shared-schema.test.ts +206 -0
  466. package/test/shell.test.ts +108 -0
  467. package/test/skill/guidance.test.ts +144 -0
  468. package/test/skill-discovery.test.ts +165 -0
  469. package/test/skill.test.ts +125 -0
  470. package/test/snapshot.test.ts +178 -0
  471. package/test/state.test.ts +115 -0
  472. package/test/system-context/builtins.test.ts +129 -0
  473. package/test/system-context/index.test.ts +307 -0
  474. package/test/system-context/registry.test.ts +114 -0
  475. package/test/tool-apply-patch.test.ts +436 -0
  476. package/test/tool-bash.test.ts +437 -0
  477. package/test/tool-edit.test.ts +434 -0
  478. package/test/tool-output-store.test.ts +247 -0
  479. package/test/tool-question.test.ts +162 -0
  480. package/test/tool-read-filesystem.test.ts +118 -0
  481. package/test/tool-read.test.ts +674 -0
  482. package/test/tool-skill.test.ts +149 -0
  483. package/test/tool-todowrite.test.ts +125 -0
  484. package/test/tool-webfetch.test.ts +281 -0
  485. package/test/tool-websearch.test.ts +316 -0
  486. package/test/tool-write.test.ts +305 -0
  487. package/test/util/effect-flock.test.ts +392 -0
  488. package/test/util/flock.test.ts +428 -0
  489. package/test/util/which.test.ts +100 -0
  490. package/tsconfig.json +7 -0
@@ -0,0 +1,260 @@
1
+ export * as WebSearchTool from "./websearch"
2
+
3
+ import { ToolFailure } from "@neurocode-ai/llm"
4
+ import { Context, Duration, Effect, Layer, Schema } from "effect"
5
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
6
+ import { makeLocationNode } from "../effect/app-node"
7
+ import { LayerNodePlatform } from "../effect/app-node-platform"
8
+ import { truthy } from "../flag/flag"
9
+ import { InstallationVersion } from "../installation/version"
10
+ import { PositiveInt } from "../schema"
11
+ import { PermissionV2 } from "../permission"
12
+ import { Tool } from "./tool"
13
+ import { Tools } from "./tools"
14
+ import { collectBoundedResponseBody } from "./http-body"
15
+ import { checksum } from "../util/encode"
16
+ import { ToolRegistry } from "./registry"
17
+
18
+ export const name = "websearch"
19
+ export const NO_RESULTS = "No search results found. Please try a different query."
20
+ export const EXA_URL = "https://mcp.exa.ai/mcp"
21
+ export const PARALLEL_URL = "https://search.parallel.ai/mcp"
22
+ export const MAX_NUM_RESULTS = 20
23
+ export const MAX_CONTEXT_CHARACTERS = 50_000
24
+ export const MAX_RESPONSE_BYTES = 256 * 1024
25
+
26
+ /**
27
+ * Provider-independent local web search retained in V2 core for launch parity.
28
+ * This invokes the legacy Exa/Parallel product backends itself. It is distinct
29
+ * from provider-hosted web search tools, which remain route-owned and execute
30
+ * at the model provider. Ownership of this compromise can be revisited later.
31
+ */
32
+ export const description = `Search the web using the session's local web search provider. Use this for current information beyond knowledge cutoff.
33
+
34
+ This is a provider-independent local tool backed by Exa or Parallel. Provider-hosted web search tools are separate and execute at the model provider.
35
+
36
+ Optional controls support result count, live crawling ('fallback' or 'preferred'), search type ('auto', 'fast', or 'deep'), and maximum context characters.
37
+
38
+ The current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.`
39
+
40
+ export const Input = Schema.Struct({
41
+ query: Schema.String.annotate({ description: "Websearch query" }),
42
+ numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({
43
+ description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`,
44
+ }),
45
+ livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
46
+ description:
47
+ "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
48
+ }),
49
+ type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
50
+ description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
51
+ }),
52
+ contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate(
53
+ {
54
+ description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
55
+ },
56
+ ),
57
+ })
58
+
59
+ export const Provider = Schema.Literals(["exa", "parallel"])
60
+ export type Provider = typeof Provider.Type
61
+
62
+ export interface Config {
63
+ readonly provider?: Provider
64
+ readonly enableExa: boolean
65
+ readonly enableParallel: boolean
66
+ readonly exaApiKey?: string
67
+ readonly parallelApiKey?: string
68
+ }
69
+
70
+ export class ConfigService extends Context.Service<ConfigService, Config>()("@neurocode/v2/WebSearchConfig") {}
71
+
72
+ /** Isolates the retained product environment contract from the generic tool implementation. */
73
+ export const defaultConfigLayer = Layer.sync(ConfigService, () =>
74
+ ConfigService.of({
75
+ provider:
76
+ process.env.NEUROCODE_WEBSEARCH_PROVIDER === "exa" || process.env.NEUROCODE_WEBSEARCH_PROVIDER === "parallel"
77
+ ? process.env.NEUROCODE_WEBSEARCH_PROVIDER
78
+ : undefined,
79
+ enableExa: truthy("NEUROCODE_EXPERIMENTAL") || truthy("NEUROCODE_ENABLE_EXA") || truthy("NEUROCODE_EXPERIMENTAL_EXA"),
80
+ enableParallel: truthy("NEUROCODE_ENABLE_PARALLEL") || truthy("NEUROCODE_EXPERIMENTAL_PARALLEL"),
81
+ exaApiKey: process.env.EXA_API_KEY,
82
+ parallelApiKey: process.env.PARALLEL_API_KEY,
83
+ }),
84
+ )
85
+
86
+ export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] })
87
+
88
+ export function selectProvider(
89
+ sessionID: string,
90
+ flags: Pick<Config, "enableExa" | "enableParallel"> = { enableExa: false, enableParallel: false },
91
+ override?: Provider,
92
+ ): Provider {
93
+ if (override) return override
94
+ if (flags.enableParallel) return "parallel"
95
+ if (flags.enableExa) return "exa"
96
+ return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel"
97
+ }
98
+
99
+ const McpResult = Schema.Struct({
100
+ result: Schema.Struct({
101
+ content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })),
102
+ }),
103
+ })
104
+ const decodeMcpResult = Schema.decodeUnknownEffect(Schema.fromJsonString(McpResult))
105
+
106
+ const parsePayload = (payload: string) =>
107
+ Effect.gen(function* () {
108
+ const trimmed = payload.trim()
109
+ if (!trimmed.startsWith("{")) return undefined
110
+ return (yield* decodeMcpResult(trimmed)).result.content.find((item) => item.text)?.text
111
+ })
112
+
113
+ export const parseResponse = Effect.fn("WebSearchTool.parseResponse")(function* (body: string) {
114
+ const trimmed = body.trim()
115
+ const direct = trimmed ? yield* parsePayload(trimmed) : undefined
116
+ if (direct) return direct
117
+ for (const line of body.split("\n")) {
118
+ if (!line.startsWith("data: ")) continue
119
+ const data = yield* parsePayload(line.substring(6))
120
+ if (data) return data
121
+ }
122
+ return undefined
123
+ })
124
+
125
+ const ExaArgs = Schema.Struct({
126
+ query: Schema.String,
127
+ type: Schema.String,
128
+ numResults: Schema.Number,
129
+ livecrawl: Schema.String,
130
+ contextMaxCharacters: Schema.optional(Schema.Number),
131
+ })
132
+ const ParallelArgs = Schema.Struct({
133
+ objective: Schema.String,
134
+ search_queries: Schema.Array(Schema.String),
135
+ session_id: Schema.String,
136
+ })
137
+ const McpRequest = <F extends Schema.Struct.Fields>(args: Schema.Struct<F>) =>
138
+ Schema.Struct({
139
+ jsonrpc: Schema.Literal("2.0"),
140
+ id: Schema.Literal(1),
141
+ method: Schema.Literal("tools/call"),
142
+ params: Schema.Struct({ name: Schema.String, arguments: args }),
143
+ })
144
+
145
+ const exaUrl = (apiKey: string | undefined) => {
146
+ if (!apiKey) return EXA_URL
147
+ const url = new URL(EXA_URL)
148
+ url.searchParams.set("exaApiKey", apiKey)
149
+ return url.toString()
150
+ }
151
+
152
+ const callMcp = <F extends Schema.Struct.Fields>(
153
+ http: HttpClient.HttpClient,
154
+ url: string,
155
+ tool: string,
156
+ args: Schema.Struct<F>,
157
+ value: Schema.Struct.Type<F>,
158
+ headers: Record<string, string> = {},
159
+ ) =>
160
+ Effect.gen(function* () {
161
+ const request = yield* HttpClientRequest.post(url).pipe(
162
+ HttpClientRequest.accept("application/json, text/event-stream"),
163
+ HttpClientRequest.setHeaders(headers),
164
+ HttpClientRequest.schemaBodyJson(McpRequest(args))({
165
+ jsonrpc: "2.0" as const,
166
+ id: 1 as const,
167
+ method: "tools/call" as const,
168
+ params: { name: tool, arguments: value },
169
+ }),
170
+ )
171
+ return yield* Effect.gen(function* () {
172
+ const response = yield* HttpClient.filterStatusOk(http).execute(request)
173
+ const body = yield* collectBoundedResponseBody(
174
+ response,
175
+ MAX_RESPONSE_BYTES,
176
+ () => new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`),
177
+ )
178
+ return yield* parseResponse(body.toString("utf8"))
179
+ }).pipe(
180
+ Effect.timeoutOrElse({
181
+ duration: Duration.seconds(25),
182
+ orElse: () => Effect.fail(new Error(`${tool} request timed out`)),
183
+ }),
184
+ )
185
+ })
186
+
187
+ const Output = Schema.Struct({
188
+ provider: Provider,
189
+ text: Schema.String,
190
+ })
191
+
192
+ const layer = Layer.effectDiscard(
193
+ Effect.gen(function* () {
194
+ const tools = yield* Tools.Service
195
+ const http = yield* HttpClient.HttpClient
196
+ const config = yield* ConfigService
197
+ const permission = yield* PermissionV2.Service
198
+
199
+ yield* tools
200
+ .register({
201
+ [name]: Tool.make({
202
+ description,
203
+ input: Input,
204
+ output: Output,
205
+ toModelOutput: ({ output }) => [{ type: "text", text: output.text }],
206
+ execute: (input, context) => {
207
+ const provider = selectProvider(context.sessionID, config, config.provider)
208
+ return Effect.gen(function* () {
209
+ yield* permission.assert({
210
+ action: name,
211
+ resources: [input.query],
212
+ save: ["*"],
213
+ metadata: { ...input, provider },
214
+ sessionID: context.sessionID,
215
+ agent: context.agent,
216
+ source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
217
+ })
218
+
219
+ const text =
220
+ provider === "exa"
221
+ ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
222
+ query: input.query,
223
+ type: input.type || "auto",
224
+ numResults: input.numResults || 8,
225
+ livecrawl: input.livecrawl || "fallback",
226
+ contextMaxCharacters: input.contextMaxCharacters,
227
+ })
228
+ : yield* callMcp(
229
+ http,
230
+ PARALLEL_URL,
231
+ "web_search",
232
+ ParallelArgs,
233
+ {
234
+ objective: input.query,
235
+ search_queries: [input.query],
236
+ session_id: context.sessionID,
237
+ // V2 invocation context does not safely expose the model yet.
238
+ },
239
+ {
240
+ "User-Agent": `opencode/${InstallationVersion}`,
241
+ ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
242
+ },
243
+ )
244
+ return {
245
+ provider,
246
+ text: text ?? NO_RESULTS,
247
+ }
248
+ }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to search the web for ${input.query}` })))
249
+ },
250
+ }),
251
+ })
252
+ .pipe(Effect.orDie)
253
+ }),
254
+ )
255
+
256
+ export const node = makeLocationNode({
257
+ name: "tool/websearch",
258
+ layer,
259
+ deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, configNode],
260
+ })
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Model-facing V2 file-write leaf. Relative paths resolve within the active
3
+ * Location. Absolute paths inside that Location are accepted, while explicit
4
+ * absolute external paths retain mutation capability through a separate
5
+ * external_directory approval before edit approval.
6
+ */
7
+ export * as WriteTool from "./write"
8
+
9
+ import { ToolFailure } from "@neurocode-ai/llm"
10
+ import { Effect, Layer, Schema } from "effect"
11
+ import { makeLocationNode } from "../effect/app-node"
12
+ import { FileMutation } from "../file-mutation"
13
+ import { LocationMutation } from "../location-mutation"
14
+ import { PermissionV2 } from "../permission"
15
+ import { ToolRegistry } from "./registry"
16
+ import { Tool } from "./tool"
17
+ import { Tools } from "./tools"
18
+
19
+ export const name = "write"
20
+
21
+ // TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
22
+ export const Input = Schema.Struct({
23
+ path: Schema.String.annotate({
24
+ description:
25
+ "File path to write. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval.",
26
+ }),
27
+ content: Schema.String.annotate({ description: "Content to write to the file" }),
28
+ })
29
+
30
+ export const Output = Schema.Struct({
31
+ operation: Schema.Literal("write"),
32
+ target: Schema.String,
33
+ resource: Schema.String,
34
+ existed: Schema.Boolean,
35
+ })
36
+ export type Output = typeof Output.Type
37
+
38
+ export const toModelOutput = (output: Output) =>
39
+ `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}`
40
+
41
+ /** Deferred V2 write UX integrations remain visible at the model-facing seam. */
42
+ // TODO: Add formatter integration after V2 formatter runtime exists.
43
+ // TODO: Publish watcher/file-edit events after V2 watcher integration exists.
44
+ // TODO: Add snapshots / undo after design exists.
45
+ // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
46
+
47
+ const layer = Layer.effectDiscard(
48
+ Effect.gen(function* () {
49
+ const tools = yield* Tools.Service
50
+ const mutation = yield* LocationMutation.Service
51
+ const files = yield* FileMutation.Service
52
+ const permission = yield* PermissionV2.Service
53
+
54
+ yield* tools
55
+ .register({
56
+ [name]: Tool.withPermission(
57
+ Tool.make({
58
+ description:
59
+ "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.",
60
+ input: Input,
61
+ output: Output,
62
+ toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
63
+ execute: (input, context) =>
64
+ Effect.gen(function* () {
65
+ const source = {
66
+ type: "tool" as const,
67
+ messageID: context.assistantMessageID,
68
+ callID: context.toolCallID,
69
+ }
70
+ const target = yield* mutation.resolve({ path: input.path, kind: "file" })
71
+ const external = target.externalDirectory
72
+ if (external)
73
+ yield* permission.assert({
74
+ ...LocationMutation.externalDirectoryPermission(external),
75
+ sessionID: context.sessionID,
76
+ agent: context.agent,
77
+ source,
78
+ })
79
+ yield* permission.assert({
80
+ action: "edit",
81
+ resources: [target.resource],
82
+ save: ["*"],
83
+ sessionID: context.sessionID,
84
+ agent: context.agent,
85
+ source,
86
+ })
87
+ return yield* files.writeTextPreservingBom({ target, content: input.content })
88
+ }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to write ${input.path}` }))),
89
+ }),
90
+ "edit",
91
+ ),
92
+ })
93
+ .pipe(Effect.orDie)
94
+ }),
95
+ )
96
+
97
+ export const node = makeLocationNode({
98
+ name: "tool/write",
99
+ layer,
100
+ deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, PermissionV2.node],
101
+ })
@@ -0,0 +1,211 @@
1
+ export * as ToolOutputStore from "./tool-output-store"
2
+
3
+ import path from "path"
4
+ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
5
+ import { Config } from "./config"
6
+ import { FSUtil } from "./fs-util"
7
+ import { Global } from "./global"
8
+ import { makeGlobalNode, makeLocationNode } from "./effect/app-node"
9
+ import { SessionSchema } from "./session/schema"
10
+ import { Identifier } from "./util/identifier"
11
+ import type { ToolOutput } from "@neurocode-ai/llm"
12
+
13
+ export const MAX_LINES = 2_000
14
+ export const MAX_BYTES = 50 * 1024
15
+ export const RETENTION = Duration.days(7)
16
+
17
+ export const MANAGED_DIRECTORY = "tool-output"
18
+
19
+ export interface BoundInput {
20
+ readonly sessionID: SessionSchema.ID
21
+ readonly toolCallID: string
22
+ readonly output: ToolOutput
23
+ }
24
+
25
+ export interface BoundResult {
26
+ readonly output: ToolOutput
27
+ readonly outputPaths: ReadonlyArray<string>
28
+ }
29
+
30
+ export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", {
31
+ operation: Schema.Literals(["encode", "write"]),
32
+ cause: Schema.Defect(),
33
+ }) {
34
+ override get message() {
35
+ const detail = this.cause instanceof Error ? this.cause.message : String(this.cause)
36
+ return `Failed to ${this.operation} tool output${detail ? `: ${detail}` : ""}`
37
+ }
38
+ }
39
+
40
+ export type Error = StorageError
41
+
42
+ export interface Interface {
43
+ readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }>
44
+ readonly bound: (input: BoundInput) => Effect.Effect<BoundResult, Error>
45
+ readonly cleanup: () => Effect.Effect<void>
46
+ }
47
+
48
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/v2/ToolOutputStore") {}
49
+
50
+ const takePrefix = (input: string, maximumBytes: number) => {
51
+ let bytes = 0
52
+ let content = ""
53
+ for (const char of input) {
54
+ const size = Buffer.byteLength(char, "utf-8")
55
+ if (bytes + size > maximumBytes) break
56
+ content += char
57
+ bytes += size
58
+ }
59
+ return content
60
+ }
61
+
62
+ const takeSuffix = (input: string, maximumBytes: number) => {
63
+ let bytes = 0
64
+ const content: string[] = []
65
+ for (const char of Array.from(input).toReversed()) {
66
+ const size = Buffer.byteLength(char, "utf-8")
67
+ if (bytes + size > maximumBytes) break
68
+ content.unshift(char)
69
+ bytes += size
70
+ }
71
+ return content.join("")
72
+ }
73
+
74
+ const preview = (text: string, maxLines: number, maxBytes: number) => {
75
+ const lines = text.split("\n")
76
+ const headLines = Math.ceil(maxLines / 2)
77
+ const tailLines = Math.floor(maxLines / 2)
78
+ const sampled =
79
+ lines.length <= maxLines
80
+ ? text
81
+ : [
82
+ lines.slice(0, headLines).join("\n"),
83
+ ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : []),
84
+ ].join("\n")
85
+ if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) {
86
+ return lines.length <= maxLines
87
+ ? { head: sampled, tail: "" }
88
+ : {
89
+ head: lines.slice(0, headLines).join("\n"),
90
+ tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "",
91
+ }
92
+ }
93
+ const headBytes = Math.ceil(maxBytes / 2)
94
+ const tailBytes = Math.floor(maxBytes / 2)
95
+ return { head: takePrefix(sampled, headBytes), tail: takeSuffix(sampled, tailBytes) }
96
+ }
97
+
98
+ const boundedPreview = (text: string, marker: string, maxLines: number, maxBytes: number) => {
99
+ const markerOnly = takePrefix(marker, maxBytes).split("\n").slice(0, maxLines).join("\n")
100
+ const markerBytes = Buffer.byteLength(marker, "utf-8")
101
+ if (maxLines <= 4 || maxBytes <= markerBytes + 4) return markerOnly
102
+ const bounded = preview(text, maxLines - 4, maxBytes - markerBytes - 4)
103
+ return bounded.tail ? `${bounded.head}\n\n${marker}\n\n${bounded.tail}` : `${bounded.head}\n\n${marker}`
104
+ }
105
+
106
+ const lineCount = (text: string) => {
107
+ let count = 1
108
+ for (const char of text) if (char === "\n") count++
109
+ return count
110
+ }
111
+
112
+ const layer = Layer.effect(
113
+ Service,
114
+ Effect.gen(function* () {
115
+ const fs = yield* FSUtil.Service
116
+ const global = yield* Global.Service
117
+ const config = yield* Effect.serviceOption(Config.Service)
118
+ const directory = path.join(global.data, MANAGED_DIRECTORY)
119
+ const limits = Effect.fn("ToolOutputStore.limits")(function* () {
120
+ if (Option.isNone(config)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES }
121
+ const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[])))
122
+ const configured = Object.assign(
123
+ {},
124
+ ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info.tool_output ?? {}] : [])),
125
+ )
126
+ return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES }
127
+ })
128
+
129
+ const write = Effect.fn("ToolOutputStore.write")(function* (content: string) {
130
+ const file = path.join(directory, `tool_${Identifier.ascending()}`)
131
+ yield* fs.ensureDir(directory).pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause })))
132
+ yield* fs
133
+ .writeFileString(file, content, { flag: "wx" })
134
+ .pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause })))
135
+ return file
136
+ })
137
+
138
+ const bound = Effect.fn("ToolOutputStore.bound")(function* (input: BoundInput) {
139
+ const outputLimits = yield* limits()
140
+ const media = input.output.content.filter((item) => item.type === "file")
141
+ const text = input.output.content.filter((item) => item.type === "text")
142
+ const contextual =
143
+ input.output.content.length === 0
144
+ ? yield* Effect.try({
145
+ try: () => JSON.stringify(input.output.structured, null, 2) ?? String(input.output.structured),
146
+ catch: (cause) => new StorageError({ operation: "encode", cause }),
147
+ })
148
+ : text.map((item) => item.text).join("")
149
+ if (
150
+ lineCount(contextual) <= outputLimits.maxLines &&
151
+ Buffer.byteLength(contextual, "utf-8") <= outputLimits.maxBytes
152
+ )
153
+ return {
154
+ output: input.output,
155
+ outputPaths: [],
156
+ }
157
+
158
+ const outputPath = yield* write(contextual)
159
+ const marker = `... output truncated; full content saved to ${outputPath} ...`
160
+
161
+ return {
162
+ output: {
163
+ structured: input.output.structured,
164
+ content: [
165
+ {
166
+ type: "text" as const,
167
+ text: boundedPreview(contextual, marker, outputLimits.maxLines, outputLimits.maxBytes),
168
+ },
169
+ ...media,
170
+ ],
171
+ },
172
+ outputPaths: [outputPath],
173
+ }
174
+ })
175
+
176
+ const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () {
177
+ const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([])))
178
+ const cutoff = Date.now() - Duration.toMillis(RETENTION)
179
+ for (const entry of entries) {
180
+ if (!entry.startsWith("tool_")) continue
181
+ const file = path.join(directory, entry)
182
+ const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.void))
183
+ const modified = info?.mtime.pipe(
184
+ Option.map((date) => date.getTime()),
185
+ Option.getOrElse(() => 0),
186
+ )
187
+ if (modified !== undefined && modified < cutoff) yield* fs.remove(file).pipe(Effect.catch(() => Effect.void))
188
+ }
189
+ })
190
+
191
+ return Service.of({ limits, bound, cleanup })
192
+ }),
193
+ )
194
+
195
+ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] })
196
+
197
+ export const nodeWithoutConfig = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node] })
198
+
199
+ /** Runs retention scanning once globally rather than once per active Location. */
200
+ export const cleanupLayer = Layer.effectDiscard(
201
+ Effect.gen(function* () {
202
+ const store = yield* Service
203
+ yield* store.cleanup().pipe(Effect.repeat(Schedule.spaced(Duration.hours(1))), Effect.forkScoped)
204
+ }),
205
+ )
206
+
207
+ export const cleanupNode = makeGlobalNode({
208
+ name: "tool-output-cleanup",
209
+ layer: Layer.merge(layer, cleanupLayer.pipe(Layer.provide(layer))),
210
+ deps: [FSUtil.node, Global.node],
211
+ })
@@ -0,0 +1,10 @@
1
+ export function findLast<T>(
2
+ items: readonly T[],
3
+ predicate: (item: T, index: number, items: readonly T[]) => boolean,
4
+ ): T | undefined {
5
+ for (let i = items.length - 1; i >= 0; i -= 1) {
6
+ const item = items[i]
7
+ if (predicate(item, i, items)) return item
8
+ }
9
+ return undefined
10
+ }
@@ -0,0 +1,41 @@
1
+ export namespace Binary {
2
+ export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
3
+ let left = 0
4
+ let right = array.length - 1
5
+
6
+ while (left <= right) {
7
+ const mid = Math.floor((left + right) / 2)
8
+ const midId = compare(array[mid])
9
+
10
+ if (midId === id) {
11
+ return { found: true, index: mid }
12
+ } else if (midId < id) {
13
+ left = mid + 1
14
+ } else {
15
+ right = mid - 1
16
+ }
17
+ }
18
+
19
+ return { found: false, index: left }
20
+ }
21
+
22
+ export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
23
+ const id = compare(item)
24
+ let left = 0
25
+ let right = array.length
26
+
27
+ while (left < right) {
28
+ const mid = Math.floor((left + right) / 2)
29
+ const midId = compare(array[mid])
30
+
31
+ if (midId < id) {
32
+ left = mid + 1
33
+ } else {
34
+ right = mid
35
+ }
36
+ }
37
+
38
+ array.splice(left, 0, item)
39
+ return array
40
+ }
41
+ }