@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,366 @@
1
+ export * as ReadToolFileSystem from "./read-filesystem"
2
+
3
+ import path from "path"
4
+ import { pathToFileURL } from "url"
5
+ import { Context, Effect, Layer, Option, Schema } from "effect"
6
+ import { FileSystem } from "../filesystem"
7
+ import { FSUtil } from "../fs-util"
8
+ import { makeLocationNode } from "../effect/app-node"
9
+ import { AbsolutePath, PositiveInt, RelativePath } from "../schema"
10
+
11
+ export const MAX_READ_LINES = 2_000
12
+ export const MAX_READ_BYTES = 50 * 1024
13
+ export const MAX_MEDIA_INGEST_BYTES = 20 * 1024 * 1024
14
+ const MAX_LINE_LENGTH = 2_000
15
+ const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`
16
+
17
+ export class BinaryFileError extends Schema.TaggedErrorClass<BinaryFileError>()("ReadTool.BinaryFileError", {
18
+ resource: Schema.String,
19
+ }) {
20
+ override get message() {
21
+ return `Cannot read binary file: ${this.resource}`
22
+ }
23
+ }
24
+
25
+ export class MediaIngestLimitError extends Schema.TaggedErrorClass<MediaIngestLimitError>()(
26
+ "ReadTool.MediaIngestLimitError",
27
+ {
28
+ resource: Schema.String,
29
+ maximumBytes: Schema.Number,
30
+ },
31
+ ) {
32
+ override get message() {
33
+ return `Media exceeds ${this.maximumBytes} byte ingestion limit: ${this.resource}`
34
+ }
35
+ }
36
+
37
+ export class MalformedUtf8Error extends Schema.TaggedErrorClass<MalformedUtf8Error>()("ReadTool.MalformedUtf8Error", {
38
+ resource: Schema.String,
39
+ }) {
40
+ override get message() {
41
+ return `File is not valid UTF-8: ${this.resource}`
42
+ }
43
+ }
44
+
45
+ export class OffsetOutOfRangeError extends Schema.TaggedErrorClass<OffsetOutOfRangeError>()(
46
+ "ReadTool.OffsetOutOfRangeError",
47
+ { offset: Schema.Number },
48
+ ) {
49
+ override get message() {
50
+ return `Offset ${this.offset} is out of range`
51
+ }
52
+ }
53
+
54
+ export class PathKindError extends Schema.TaggedErrorClass<PathKindError>()("ReadTool.PathKindError", {
55
+ resource: Schema.String,
56
+ expected: Schema.Literals(["a file", "a file or directory"]),
57
+ }) {
58
+ override get message() {
59
+ return `Path is not ${this.expected}: ${this.resource}`
60
+ }
61
+ }
62
+
63
+ export type InspectError = FSUtil.Error | PathKindError
64
+ export type ReadError =
65
+ | FSUtil.Error
66
+ | BinaryFileError
67
+ | MediaIngestLimitError
68
+ | MalformedUtf8Error
69
+ | OffsetOutOfRangeError
70
+ | PathKindError
71
+
72
+ export const PageInput = Schema.Struct({
73
+ offset: PositiveInt.pipe(Schema.optional),
74
+ limit: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_READ_LINES)).pipe(Schema.optional),
75
+ })
76
+ export type PageInput = typeof PageInput.Type
77
+
78
+ export class TextPage extends Schema.Class<TextPage>("ReadTool.TextPage")({
79
+ type: Schema.Literal("text-page"),
80
+ content: Schema.String,
81
+ mime: Schema.String,
82
+ offset: PositiveInt,
83
+ truncated: Schema.Boolean,
84
+ next: PositiveInt.pipe(Schema.optional),
85
+ }) {}
86
+
87
+ export class ListPage extends Schema.Class<ListPage>("ReadTool.ListPage")({
88
+ entries: Schema.Array(FileSystem.Entry),
89
+ truncated: Schema.Boolean,
90
+ next: PositiveInt.pipe(Schema.optional),
91
+ }) {}
92
+
93
+ export interface Interface {
94
+ readonly inspect: (path: AbsolutePath) => Effect.Effect<"file" | "directory", InspectError>
95
+ readonly read: (
96
+ path: AbsolutePath,
97
+ resource: string,
98
+ page?: PageInput,
99
+ ) => Effect.Effect<FileSystem.Content | TextPage, ReadError>
100
+ readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect<ListPage, FSUtil.Error>
101
+ }
102
+
103
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/ReadToolFileSystem") {}
104
+
105
+ const extensions = new Set([
106
+ ".zip",
107
+ ".tar",
108
+ ".gz",
109
+ ".exe",
110
+ ".dll",
111
+ ".so",
112
+ ".class",
113
+ ".jar",
114
+ ".war",
115
+ ".7z",
116
+ ".doc",
117
+ ".docx",
118
+ ".xls",
119
+ ".xlsx",
120
+ ".ppt",
121
+ ".pptx",
122
+ ".odt",
123
+ ".ods",
124
+ ".odp",
125
+ ".bin",
126
+ ".dat",
127
+ ".obj",
128
+ ".o",
129
+ ".a",
130
+ ".lib",
131
+ ".wasm",
132
+ ".pyc",
133
+ ".pyo",
134
+ ])
135
+ const startsWith = (bytes: Uint8Array, prefix: number[]) => prefix.every((value, index) => bytes[index] === value)
136
+ const imageMime = (bytes: Uint8Array) => {
137
+ if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png"
138
+ if (startsWith(bytes, [0xff, 0xd8, 0xff])) return "image/jpeg"
139
+ if (startsWith(bytes, [0x47, 0x49, 0x46, 0x38])) return "image/gif"
140
+ if (startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes.subarray(8), [0x57, 0x45, 0x42, 0x50]))
141
+ return "image/webp"
142
+ }
143
+ const binary = (resource: string, bytes: Uint8Array) => {
144
+ if (extensions.has(path.extname(resource).toLowerCase())) return true
145
+ if (bytes.length === 0) return false
146
+ let nonPrintable = 0
147
+ for (const byte of bytes) {
148
+ if (byte === 0) return true
149
+ if (byte < 9 || (byte > 13 && byte < 32)) nonPrintable++
150
+ }
151
+ return nonPrintable / bytes.length > 0.3
152
+ }
153
+ const decodeUtf8 = (resource: string, decoder: TextDecoder, bytes?: Uint8Array) =>
154
+ Effect.try({
155
+ try: () => decoder.decode(bytes, { stream: bytes !== undefined }),
156
+ catch: (error) => {
157
+ if (error instanceof TypeError) return new MalformedUtf8Error({ resource })
158
+ throw error
159
+ },
160
+ })
161
+ const decodeChunk = (resource: string, decoder: TextDecoder, bytes: Uint8Array) =>
162
+ bytes.includes(0) ? Effect.fail(new BinaryFileError({ resource })) : decodeUtf8(resource, decoder, bytes)
163
+
164
+ export const inspect = Effect.fn("ReadTool.inspect")(function* (fs: FSUtil.Interface, input: string) {
165
+ const info = yield* fs.stat(input)
166
+ const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined
167
+ if (!type) return yield* Effect.fail(new PathKindError({ resource: input, expected: "a file or directory" }))
168
+ return type
169
+ })
170
+
171
+ export const read = Effect.fn("ReadTool.read")(function* (
172
+ fs: FSUtil.Interface,
173
+ input: string,
174
+ resource: string,
175
+ page: PageInput = {},
176
+ ) {
177
+ const real = yield* fs.realPath(input)
178
+ return yield* Effect.scoped(
179
+ Effect.gen(function* () {
180
+ const file = yield* fs.open(real, { flag: "r" })
181
+ const info = yield* file.stat
182
+ if (info.type !== "File") return yield* Effect.fail(new PathKindError({ resource, expected: "a file" }))
183
+ const first = Option.getOrElse(
184
+ yield* file.readAlloc(Math.min(64 * 1024, Number(info.size) || 4 * 1024)),
185
+ () => new Uint8Array(),
186
+ )
187
+ const mime = imageMime(first)
188
+ if (mime) {
189
+ if (info.size > MAX_MEDIA_INGEST_BYTES)
190
+ return yield* Effect.fail(new MediaIngestLimitError({ resource, maximumBytes: MAX_MEDIA_INGEST_BYTES }))
191
+ const chunks = [first]
192
+ let total = first.length
193
+ while (total <= MAX_MEDIA_INGEST_BYTES) {
194
+ const chunk = yield* file.readAlloc(Math.min(64 * 1024, MAX_MEDIA_INGEST_BYTES + 1 - total))
195
+ if (Option.isNone(chunk)) break
196
+ chunks.push(chunk.value)
197
+ total += chunk.value.length
198
+ }
199
+ if (total > MAX_MEDIA_INGEST_BYTES)
200
+ return yield* Effect.fail(new MediaIngestLimitError({ resource, maximumBytes: MAX_MEDIA_INGEST_BYTES }))
201
+ return {
202
+ uri: pathToFileURL(real).href,
203
+ name: path.basename(real),
204
+ content: Buffer.concat(
205
+ chunks.map((chunk) => Buffer.from(chunk)),
206
+ total,
207
+ ).toString("base64"),
208
+ encoding: "base64" as const,
209
+ mime,
210
+ }
211
+ }
212
+ if (startsWith(first, [0x25, 0x50, 0x44, 0x46]) || extensions.has(path.extname(resource).toLowerCase()))
213
+ return yield* Effect.fail(new BinaryFileError({ resource }))
214
+ const paged = info.size > MAX_READ_BYTES || page.offset !== undefined || page.limit !== undefined
215
+ if (!paged) {
216
+ if (binary(resource, first)) return yield* Effect.fail(new BinaryFileError({ resource }))
217
+ const decoder = new TextDecoder("utf-8", { fatal: true })
218
+ const text = [yield* decodeUtf8(resource, decoder, first)]
219
+ while (true) {
220
+ const chunk = yield* file.readAlloc(64 * 1024)
221
+ if (Option.isNone(chunk)) break
222
+ text.push(yield* decodeChunk(resource, decoder, chunk.value))
223
+ }
224
+ text.push(yield* decodeUtf8(resource, decoder))
225
+ return {
226
+ uri: pathToFileURL(real).href,
227
+ name: path.basename(real),
228
+ content: text.join(""),
229
+ encoding: "utf8" as const,
230
+ mime: FSUtil.mimeType(real),
231
+ }
232
+ }
233
+ const offset = page.offset ?? 1
234
+ const limit = Math.min(page.limit ?? MAX_READ_LINES, MAX_READ_LINES)
235
+ const lines: string[] = []
236
+ const decoder = new TextDecoder("utf-8", { fatal: true })
237
+ let pending = ""
238
+ let discard = false
239
+ let line = 1
240
+ let bytes = 0
241
+ let next: number | undefined
242
+ const append = (input: string) => {
243
+ if (line < offset) {
244
+ line++
245
+ return true
246
+ }
247
+ if (lines.length >= limit || bytes >= MAX_READ_BYTES) {
248
+ next = line
249
+ return false
250
+ }
251
+ const text = input.length > MAX_LINE_LENGTH ? input.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : input
252
+ const size = Buffer.byteLength(text, "utf-8") + (lines.length > 0 ? 1 : 0)
253
+ if (bytes + size > MAX_READ_BYTES) {
254
+ next = line
255
+ return false
256
+ }
257
+ lines.push(text)
258
+ bytes += size
259
+ line++
260
+ return true
261
+ }
262
+ const consume = (input: string) => {
263
+ let text = input
264
+ while (true) {
265
+ const index = text.indexOf("\n")
266
+ if (index === -1) {
267
+ if (!discard) {
268
+ pending += text
269
+ if (pending.length > MAX_LINE_LENGTH) {
270
+ pending = pending.slice(0, MAX_LINE_LENGTH + 1)
271
+ discard = true
272
+ }
273
+ }
274
+ break
275
+ }
276
+ const current = pending + (discard ? "" : text.slice(0, index))
277
+ pending = ""
278
+ discard = false
279
+ text = text.slice(index + 1)
280
+ if (!append(current.endsWith("\r") ? current.slice(0, -1) : current)) return false
281
+ }
282
+ return true
283
+ }
284
+ const consumeChunk = Effect.fnUntraced(function* (chunk: Uint8Array) {
285
+ let start = 0
286
+ while (start < chunk.length) {
287
+ if (lines.length >= limit || bytes >= MAX_READ_BYTES) {
288
+ next = line
289
+ return false
290
+ }
291
+ const newline = chunk.indexOf(10, start)
292
+ const end = newline === -1 ? chunk.length : newline + 1
293
+ const segment = chunk.subarray(start, end)
294
+ if (binary(resource, segment)) return yield* Effect.fail(new BinaryFileError({ resource }))
295
+ if (!consume(yield* decodeUtf8(resource, decoder, segment))) return false
296
+ start = end
297
+ }
298
+ return true
299
+ })
300
+ let done = !(yield* consumeChunk(first))
301
+ while (!done) {
302
+ const chunk = yield* file.readAlloc(64 * 1024)
303
+ if (Option.isNone(chunk)) break
304
+ done = !(yield* consumeChunk(chunk.value))
305
+ }
306
+ if (!done) {
307
+ const tail = yield* decodeUtf8(resource, decoder)
308
+ if (!discard) pending += tail
309
+ if (pending) append(pending.endsWith("\r") ? pending.slice(0, -1) : pending)
310
+ }
311
+ if (lines.length === 0 && offset !== 1) return yield* Effect.fail(new OffsetOutOfRangeError({ offset }))
312
+ return new TextPage({
313
+ type: "text-page",
314
+ content: lines.join("\n"),
315
+ mime: FSUtil.mimeType(real),
316
+ offset,
317
+ truncated: next !== undefined,
318
+ ...(next === undefined ? {} : { next }),
319
+ })
320
+ }),
321
+ )
322
+ })
323
+
324
+ export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface, input: string, page: PageInput = {}) {
325
+ const real = yield* fs.realPath(input)
326
+ const items = yield* fs.readDirectoryEntries(real)
327
+ const offset = page.offset ?? 1
328
+ const limit = Math.min(page.limit ?? MAX_READ_LINES, MAX_READ_LINES)
329
+ const entries = yield* Effect.forEach(
330
+ items,
331
+ (item) =>
332
+ Effect.gen(function* () {
333
+ const absolute = path.join(real, item.name)
334
+ const target = yield* fs.realPath(absolute).pipe(Effect.catch(() => Effect.void))
335
+ if (!target || !FSUtil.contains(real, target)) return
336
+ const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.void))
337
+ const type = info?.type === "Directory" ? "directory" : info?.type === "File" ? "file" : undefined
338
+ if (!type) return
339
+ return FileSystem.Entry.make({
340
+ path: RelativePath.make(item.name + (type === "directory" ? path.sep : "")),
341
+ type,
342
+ })
343
+ }),
344
+ { concurrency: 16 },
345
+ )
346
+ const visible = entries
347
+ .filter((item): item is FileSystem.Entry => item !== undefined)
348
+ .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1))
349
+ const selected = visible.slice(offset - 1, offset - 1 + limit)
350
+ const truncated = offset - 1 + selected.length < visible.length
351
+ return new ListPage({ entries: selected, truncated, ...(truncated ? { next: offset + selected.length } : {}) })
352
+ })
353
+
354
+ const layer = Layer.effect(
355
+ Service,
356
+ Effect.gen(function* () {
357
+ const fs = yield* FSUtil.Service
358
+ return Service.of({
359
+ inspect: (path) => inspect(fs, path),
360
+ read: (path, resource, page) => read(fs, path, resource, page),
361
+ list: (path, page) => list(fs, path, page),
362
+ })
363
+ }),
364
+ )
365
+
366
+ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
@@ -0,0 +1,117 @@
1
+ export * as ReadTool from "./read"
2
+
3
+ import { ToolFailure } from "@neurocode-ai/llm"
4
+ import { Effect, Layer, Schema } from "effect"
5
+ import { makeLocationNode } from "../effect/app-node"
6
+ import { FileSystem } from "../filesystem"
7
+ import { Image } from "../image"
8
+ import { LocationMutation } from "../location-mutation"
9
+ import { PermissionV2 } from "../permission"
10
+ import { AbsolutePath } from "../schema"
11
+ import { ReadToolFileSystem } from "./read-filesystem"
12
+ import { ToolRegistry } from "./registry"
13
+ import { Tool } from "./tool"
14
+ import { Tools } from "./tools"
15
+
16
+ export const name = "read"
17
+ const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"])
18
+ const LocationInput = Schema.Struct({
19
+ path: Schema.String,
20
+ offset: ReadToolFileSystem.PageInput.fields.offset.annotate({
21
+ description: "The 1-based directory entry or text line offset to start reading from",
22
+ }),
23
+ limit: ReadToolFileSystem.PageInput.fields.limit.annotate({
24
+ description: "The maximum number of directory entries or text lines to read",
25
+ }),
26
+ })
27
+ const Input = LocationInput
28
+ const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
29
+
30
+ const layer = Layer.effectDiscard(
31
+ Effect.gen(function* () {
32
+ const tools = yield* Tools.Service
33
+ const reader = yield* ReadToolFileSystem.Service
34
+ const mutation = yield* LocationMutation.Service
35
+ const image = yield* Image.Service
36
+ const permission = yield* PermissionV2.Service
37
+
38
+ yield* tools
39
+ .register({
40
+ [name]: Tool.make({
41
+ description:
42
+ "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval.",
43
+ input: Input,
44
+ output: Output,
45
+ toModelOutput: ({ input, output }) => {
46
+ if (!("encoding" in output) || output.encoding !== "base64" || !SUPPORTED_IMAGE_MIMES.has(output.mime))
47
+ return []
48
+ return [
49
+ { type: "text", text: "Image read successfully" },
50
+ { type: "file", data: output.content, mime: output.mime, name: input.path },
51
+ ]
52
+ },
53
+ execute: (input, context) => {
54
+ return Effect.gen(function* () {
55
+ const source = {
56
+ type: "tool" as const,
57
+ messageID: context.assistantMessageID,
58
+ callID: context.toolCallID,
59
+ }
60
+ const target = yield* mutation.resolve({ path: input.path, kind: "directory" })
61
+ const external = target.externalDirectory
62
+ if (external)
63
+ yield* permission.assert({
64
+ ...LocationMutation.externalDirectoryPermission(external),
65
+ sessionID: context.sessionID,
66
+ agent: context.agent,
67
+ source,
68
+ })
69
+ const resource = target.resource
70
+ const absolute = AbsolutePath.make(target.canonical)
71
+ const type = yield* reader.inspect(absolute)
72
+ yield* permission.assert({
73
+ action: name,
74
+ resources: [resource],
75
+ save: ["*"],
76
+ sessionID: context.sessionID,
77
+ agent: context.agent,
78
+ source,
79
+ })
80
+ if (type === "directory")
81
+ return yield* reader.list(absolute, { offset: input.offset, limit: input.limit })
82
+ const content = yield* reader.read(absolute, resource, {
83
+ offset: input.offset,
84
+ limit: input.limit,
85
+ })
86
+ if ("encoding" in content && content.encoding === "base64" && SUPPORTED_IMAGE_MIMES.has(content.mime)) {
87
+ return yield* image
88
+ .normalize(resource, { ...content, encoding: "base64" })
89
+ .pipe(Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(content)))
90
+ }
91
+ if ("encoding" in content && content.encoding === "base64")
92
+ return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource }))
93
+ return content
94
+ }).pipe(
95
+ Effect.mapError((error) => {
96
+ const message =
97
+ error instanceof ReadToolFileSystem.BinaryFileError ||
98
+ error instanceof ReadToolFileSystem.MediaIngestLimitError ||
99
+ error instanceof Image.DecodeError ||
100
+ error instanceof Image.SizeError
101
+ ? error.message
102
+ : `Unable to read ${input.path}`
103
+ return new ToolFailure({ message })
104
+ }),
105
+ )
106
+ },
107
+ }),
108
+ })
109
+ .pipe(Effect.orDie)
110
+ }),
111
+ )
112
+
113
+ export const node = makeLocationNode({
114
+ name: "tool/read",
115
+ layer,
116
+ deps: [ToolRegistry.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, PermissionV2.node],
117
+ })
@@ -0,0 +1,147 @@
1
+ export * as ToolRegistry from "./registry"
2
+
3
+ import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@neurocode-ai/llm"
4
+ import { Context, Effect, Layer, Scope } from "effect"
5
+ import { AgentV2 } from "../agent"
6
+ import { PermissionV2 } from "../permission"
7
+ import { SessionMessage } from "../session/message"
8
+ import { SessionSchema } from "../session/schema"
9
+ import { ToolOutputStore } from "../tool-output-store"
10
+ import { Wildcard } from "../util/wildcard"
11
+ import { ApplicationTools } from "./application-tools"
12
+ import { definition, permission, settle, validateName, type AnyTool, type RegistrationError } from "./tool"
13
+ import { Tools } from "./tools"
14
+ import { makeLocationNode } from "../effect/app-node"
15
+
16
+ export type ExecuteInput = {
17
+ readonly sessionID: SessionSchema.ID
18
+ readonly agent: AgentV2.ID
19
+ readonly assistantMessageID: SessionMessage.ID
20
+ readonly call: ToolCall
21
+ }
22
+
23
+ export interface Interface {
24
+ readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect<Materialization>
25
+ /** Internal registration capability exposed publicly only through Tools.Service. */
26
+ readonly register: (tools: Readonly<Record<string, AnyTool>>) => Effect.Effect<void, RegistrationError, Scope.Scope>
27
+ }
28
+
29
+ export interface Materialization {
30
+ readonly definitions: ReadonlyArray<ToolDefinition>
31
+ readonly settle: (input: ExecuteInput) => Effect.Effect<Settlement, ToolOutputStore.Error>
32
+ }
33
+
34
+ export interface Settlement {
35
+ readonly result: ToolResultValue
36
+ readonly output?: ToolOutput
37
+ readonly outputPaths?: ReadonlyArray<string>
38
+ }
39
+
40
+ export class Service extends Context.Service<Service, Interface>()("@neurocode/v2/ToolRegistry") {}
41
+
42
+ const registryLayer = Layer.effect(
43
+ Service,
44
+ Effect.gen(function* () {
45
+ const applications = yield* ApplicationTools.Service
46
+ const resources = yield* ToolOutputStore.Service
47
+ type Registration = { readonly identity: object; readonly tool: AnyTool }
48
+ const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
49
+
50
+ const settleWith = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput, advertised?: object) {
51
+ const registration =
52
+ local.get(input.call.name)?.at(-1)?.registration ?? applications.entries().get(input.call.name)
53
+ if (!registration)
54
+ return {
55
+ result: {
56
+ type: "error" as const,
57
+ value: advertised ? `Stale tool call: ${input.call.name}` : `Unknown tool: ${input.call.name}`,
58
+ },
59
+ }
60
+ if (advertised && registration.identity !== advertised)
61
+ return { result: { type: "error" as const, value: `Stale tool call: ${input.call.name}` } }
62
+ const pending = yield* settle(registration.tool, input.call, {
63
+ sessionID: input.sessionID,
64
+ agent: input.agent,
65
+ assistantMessageID: input.assistantMessageID,
66
+ toolCallID: input.call.id,
67
+ }).pipe(
68
+ Effect.map((output) => ({ output })),
69
+ Effect.catchTag("LLM.ToolFailure", (failure) =>
70
+ Effect.succeed({ result: { type: "error" as const, value: failure.message } }),
71
+ ),
72
+ )
73
+ if ("result" in pending) return pending
74
+ const output = pending.output
75
+ const bounded = yield* resources.bound({ sessionID: input.sessionID, toolCallID: input.call.id, output })
76
+ const result = ToolOutput.toResultValue(bounded.output)
77
+ if (result.type === "error")
78
+ return bounded.outputPaths.length > 0 ? { result, outputPaths: bounded.outputPaths } : { result }
79
+ return bounded.outputPaths.length > 0
80
+ ? { result, output: bounded.output, outputPaths: bounded.outputPaths }
81
+ : { result, output: bounded.output }
82
+ })
83
+
84
+ return Service.of({
85
+ register: Effect.fn("ToolRegistry.register")(function* (tools) {
86
+ const entries = Object.entries(tools)
87
+ if (entries.length === 0) return
88
+ yield* Effect.forEach(entries, ([name]) => validateName(name), { discard: true })
89
+ yield* Effect.uninterruptible(
90
+ Effect.gen(function* () {
91
+ const token = {}
92
+ for (const [name, tool] of entries)
93
+ local.set(name, [...(local.get(name) ?? []), { token, registration: { identity: {}, tool } }])
94
+ yield* Effect.addFinalizer(() =>
95
+ Effect.sync(() => {
96
+ for (const [name] of entries) {
97
+ const registrations = local.get(name)?.filter((registration) => registration.token !== token) ?? []
98
+ if (registrations.length > 0) local.set(name, registrations)
99
+ else local.delete(name)
100
+ }
101
+ }),
102
+ )
103
+ }),
104
+ )
105
+ }),
106
+ materialize: Effect.fn("ToolRegistry.materialize")(function* (permissions = []) {
107
+ const registrations = new Map(applications.entries())
108
+ for (const [name, entries] of local) {
109
+ const registration = entries.at(-1)?.registration
110
+ if (registration) registrations.set(name, registration)
111
+ }
112
+ for (const [name, registration] of registrations)
113
+ if (whollyDisabled(permission(registration.tool, name), permissions)) registrations.delete(name)
114
+ return {
115
+ definitions: Array.from(registrations, ([name, registration]) => definition(name, registration.tool)),
116
+ settle: (input) => {
117
+ const registration = registrations.get(input.call.name)
118
+ if (registration) return settleWith(input, registration.identity)
119
+ return Effect.succeed({ result: { type: "error", value: `Unknown tool: ${input.call.name}` } })
120
+ },
121
+ }
122
+ }),
123
+ })
124
+ }),
125
+ )
126
+
127
+ const layer = Layer.effect(
128
+ Tools.Service,
129
+ Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
130
+ ).pipe(Layer.provideMerge(registryLayer))
131
+
132
+ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
133
+ const rule = rules.findLast((rule) => Wildcard.match(action, rule.action))
134
+ return rule?.resource === "*" && rule.effect === "deny"
135
+ }
136
+
137
+ export const node = makeLocationNode({
138
+ service: Service,
139
+ layer,
140
+ deps: [ApplicationTools.node, ToolOutputStore.node],
141
+ })
142
+
143
+ export const toolsNode = makeLocationNode({
144
+ service: Tools.Service,
145
+ layer,
146
+ deps: [ApplicationTools.node, ToolOutputStore.node],
147
+ })