@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.
- package/bin/neurocode +199 -0
- package/bunfig.toml +2 -0
- package/drizzle.config.ts +10 -0
- package/package.json +131 -0
- package/schema.json +2071 -0
- package/script/fix-node-pty.ts +28 -0
- package/script/migration.ts +196 -0
- package/src/account/sql.ts +39 -0
- package/src/account.ts +101 -0
- package/src/agent.ts +111 -0
- package/src/aisdk.ts +235 -0
- package/src/background-job.ts +365 -0
- package/src/catalog.ts +301 -0
- package/src/command.ts +64 -0
- package/src/config/agent.ts +25 -0
- package/src/config/attachments.ts +15 -0
- package/src/config/command.ts +12 -0
- package/src/config/compaction.ts +15 -0
- package/src/config/experimental.ts +18 -0
- package/src/config/formatter.ts +12 -0
- package/src/config/lsp.ts +18 -0
- package/src/config/markdown.ts +36 -0
- package/src/config/mcp.ts +48 -0
- package/src/config/plugin/agent.ts +179 -0
- package/src/config/plugin/command.ts +83 -0
- package/src/config/plugin/external.ts +91 -0
- package/src/config/plugin/provider.ts +113 -0
- package/src/config/plugin/reference.ts +70 -0
- package/src/config/plugin/skill.ts +50 -0
- package/src/config/plugin.ts +13 -0
- package/src/config/provider.ts +71 -0
- package/src/config/reference.ts +22 -0
- package/src/config/tool-output.ts +9 -0
- package/src/config/watcher.ts +7 -0
- package/src/config.ts +227 -0
- package/src/control-plane/move-session.ts +148 -0
- package/src/control-plane/workspace.sql.ts +20 -0
- package/src/credential/sql.ts +14 -0
- package/src/credential.ts +138 -0
- package/src/cross-spawn-spawner.ts +507 -0
- package/src/data-migration.sql.ts +6 -0
- package/src/database/database.ts +57 -0
- package/src/database/migration/20260127222353_familiar_lady_ursula.ts +107 -0
- package/src/database/migration/20260211171708_add_project_commands.ts +11 -0
- package/src/database/migration/20260213144116_wakeful_the_professor.ts +23 -0
- package/src/database/migration/20260225215848_workspace.ts +19 -0
- package/src/database/migration/20260227213759_add_session_workspace_id.ts +12 -0
- package/src/database/migration/20260228203230_blue_harpoon.ts +30 -0
- package/src/database/migration/20260303231226_add_workspace_fields.ts +15 -0
- package/src/database/migration/20260309230000_move_org_to_state.ts +15 -0
- package/src/database/migration/20260312043431_session_message_cursor.ts +16 -0
- package/src/database/migration/20260323234822_events.ts +26 -0
- package/src/database/migration/20260410174513_workspace-name.ts +29 -0
- package/src/database/migration/20260413175956_chief_energizer.ts +24 -0
- package/src/database/migration/20260423070820_add_icon_url_override.ts +14 -0
- package/src/database/migration/20260427172553_slow_nightmare.ts +30 -0
- package/src/database/migration/20260428004200_add_session_path.ts +11 -0
- package/src/database/migration/20260501142318_next_venus.ts +12 -0
- package/src/database/migration/20260504145000_add_sync_owner.ts +11 -0
- package/src/database/migration/20260507164347_add_workspace_time.ts +11 -0
- package/src/database/migration/20260510033149_session_usage.ts +56 -0
- package/src/database/migration/20260511000411_data_migration_state.ts +16 -0
- package/src/database/migration/20260511173437_session-metadata.ts +16 -0
- package/src/database/migration/20260601010001_normalize_storage_paths.ts +22 -0
- package/src/database/migration/20260601202201_amazing_prowler.ts +11 -0
- package/src/database/migration/20260602002951_lowly_union_jack.ts +24 -0
- package/src/database/migration/20260602182828_add_project_directories.ts +20 -0
- package/src/database/migration/20260603001617_session_message_projection_indexes.ts +19 -0
- package/src/database/migration/20260603040000_session_message_projection_order.ts +19 -0
- package/src/database/migration/20260603141458_session_input_inbox.ts +25 -0
- package/src/database/migration/20260603160727_jittery_ezekiel_stane.ts +20 -0
- package/src/database/migration/20260604172448_event_sourced_session_input.ts +47 -0
- package/src/database/migration/20260605003541_add_session_context_snapshot.ts +21 -0
- package/src/database/migration/20260605042240_add_context_epoch_agent.ts +11 -0
- package/src/database/migration/20260611035744_credential.ts +25 -0
- package/src/database/migration/20260611192811_lush_chimera.ts +25 -0
- package/src/database/migration/20260612174303_project_dir_strategy.ts +29 -0
- package/src/database/migration/20260622142730_simplify_session_context_epoch.ts +13 -0
- package/src/database/migration/20260622170816_reset_v2_session_state.ts +17 -0
- package/src/database/migration/20260622202450_simplify_session_input.ts +17 -0
- package/src/database/migration.gen.ts +44 -0
- package/src/database/migration.ts +81 -0
- package/src/database/path.ts +91 -0
- package/src/database/schema.gen.ts +274 -0
- package/src/database/schema.sql.ts +10 -0
- package/src/database/sqlite.bun.ts +183 -0
- package/src/database/sqlite.node.ts +178 -0
- package/src/database/sqlite.ts +8 -0
- package/src/effect/app-node-builder.ts +23 -0
- package/src/effect/app-node-platform.ts +18 -0
- package/src/effect/app-node.ts +14 -0
- package/src/effect/dfdf +1 -0
- package/src/effect/keyed-mutex.ts +45 -0
- package/src/effect/layer-node.ts +333 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/runtime.ts +21 -0
- package/src/effect/service-use.ts +43 -0
- package/src/event/sql.ts +25 -0
- package/src/event.ts +638 -0
- package/src/file-mutation.ts +207 -0
- package/src/file.ts +6 -0
- package/src/filesystem/fff.bun.ts +140 -0
- package/src/filesystem/fff.node.ts +138 -0
- package/src/filesystem/ignore.ts +67 -0
- package/src/filesystem/protected.ts +53 -0
- package/src/filesystem/search.ts +239 -0
- package/src/filesystem/watcher.ts +140 -0
- package/src/filesystem.ts +118 -0
- package/src/flag/flag.ts +78 -0
- package/src/fs-util.ts +274 -0
- package/src/git.ts +987 -0
- package/src/github-copilot/README.md +5 -0
- package/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
- package/src/github-copilot/chat/get-response-metadata.ts +15 -0
- package/src/github-copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/github-copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/github-copilot/chat/openai-compatible-chat-language-model.ts +815 -0
- package/src/github-copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/github-copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/github-copilot/chat/openai-compatible-prepare-tools.ts +83 -0
- package/src/github-copilot/copilot-provider.ts +100 -0
- package/src/github-copilot/openai-compatible-error.ts +27 -0
- package/src/github-copilot/responses/convert-to-openai-responses-input.ts +335 -0
- package/src/github-copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/github-copilot/responses/openai-config.ts +18 -0
- package/src/github-copilot/responses/openai-error.ts +22 -0
- package/src/github-copilot/responses/openai-responses-api-types.ts +214 -0
- package/src/github-copilot/responses/openai-responses-language-model.ts +1770 -0
- package/src/github-copilot/responses/openai-responses-prepare-tools.ts +173 -0
- package/src/github-copilot/responses/openai-responses-settings.ts +1 -0
- package/src/github-copilot/responses/tool/code-interpreter.ts +87 -0
- package/src/github-copilot/responses/tool/file-search.ts +127 -0
- package/src/github-copilot/responses/tool/image-generation.ts +114 -0
- package/src/github-copilot/responses/tool/local-shell.ts +64 -0
- package/src/github-copilot/responses/tool/web-search-preview.ts +103 -0
- package/src/github-copilot/responses/tool/web-search.ts +102 -0
- package/src/global.ts +87 -0
- package/src/id/id.ts +47 -0
- package/src/image/photon.ts +94 -0
- package/src/image.ts +81 -0
- package/src/installation/version.ts +8 -0
- package/src/instruction-context.ts +101 -0
- package/src/integration/connection.ts +12 -0
- package/src/integration.ts +520 -0
- package/src/location-mutation.ts +162 -0
- package/src/location-service-map.ts +18 -0
- package/src/location-services.ts +115 -0
- package/src/location.ts +39 -0
- package/src/markdown.d.ts +4 -0
- package/src/model.ts +41 -0
- package/src/models-dev.ts +260 -0
- package/src/npm-config.ts +40 -0
- package/src/npm.ts +269 -0
- package/src/oauth/page.ts +276 -0
- package/src/observability/logging.ts +71 -0
- package/src/observability/otlp.ts +79 -0
- package/src/observability/shared.ts +1 -0
- package/src/observability.ts +24 -0
- package/src/patch.ts +197 -0
- package/src/permission/saved.ts +79 -0
- package/src/permission/sql.ts +20 -0
- package/src/permission.ts +310 -0
- package/src/plugin/agent.ts +206 -0
- package/src/plugin/command/initialize.txt +65 -0
- package/src/plugin/command/review.txt +100 -0
- package/src/plugin/command.ts +25 -0
- package/src/plugin/host.ts +219 -0
- package/src/plugin/internal.ts +153 -0
- package/src/plugin/layer-map.example.ts +94 -0
- package/src/plugin/models-dev.ts +183 -0
- package/src/plugin/promise.ts +93 -0
- package/src/plugin/provider/alibaba.ts +15 -0
- package/src/plugin/provider/amazon-bedrock.ts +126 -0
- package/src/plugin/provider/anthropic.ts +27 -0
- package/src/plugin/provider/azure.ts +82 -0
- package/src/plugin/provider/cerebras.ts +26 -0
- package/src/plugin/provider/cloudflare-ai-gateway.ts +81 -0
- package/src/plugin/provider/cloudflare-workers-ai.ts +81 -0
- package/src/plugin/provider/cohere.ts +15 -0
- package/src/plugin/provider/deepinfra.ts +15 -0
- package/src/plugin/provider/dynamic.ts +31 -0
- package/src/plugin/provider/gateway.ts +15 -0
- package/src/plugin/provider/github-copilot.ts +52 -0
- package/src/plugin/provider/gitlab.ts +65 -0
- package/src/plugin/provider/google-vertex.ts +173 -0
- package/src/plugin/provider/google.ts +15 -0
- package/src/plugin/provider/groq.ts +15 -0
- package/src/plugin/provider/kilo.ts +21 -0
- package/src/plugin/provider/llmgateway.ts +26 -0
- package/src/plugin/provider/mistral.ts +15 -0
- package/src/plugin/provider/nvidia.ts +22 -0
- package/src/plugin/provider/openai-compatible.ts +17 -0
- package/src/plugin/provider/openai.ts +292 -0
- package/src/plugin/provider/opencode.ts +311 -0
- package/src/plugin/provider/openrouter.ts +36 -0
- package/src/plugin/provider/perplexity.ts +15 -0
- package/src/plugin/provider/sap-ai-core.ts +46 -0
- package/src/plugin/provider/snowflake-cortex.ts +89 -0
- package/src/plugin/provider/togetherai.ts +15 -0
- package/src/plugin/provider/venice.ts +15 -0
- package/src/plugin/provider/vercel.ts +27 -0
- package/src/plugin/provider/xai.ts +22 -0
- package/src/plugin/provider/zenmux.ts +21 -0
- package/src/plugin/provider.ts +71 -0
- package/src/plugin/skill/customize-neurocode.md +452 -0
- package/src/plugin/skill.ts +31 -0
- package/src/plugin/variant.ts +39 -0
- package/src/plugin.ts +167 -0
- package/src/policy.ts +49 -0
- package/src/process.ts +261 -0
- package/src/project/copy-strategies.ts +35 -0
- package/src/project/copy.ts +292 -0
- package/src/project/directories.ts +158 -0
- package/src/project/schema.ts +16 -0
- package/src/project/sql.ts +35 -0
- package/src/project.ts +136 -0
- package/src/provider.ts +25 -0
- package/src/pty/protocol.ts +37 -0
- package/src/pty/pty.bun.ts +26 -0
- package/src/pty/pty.node.ts +29 -0
- package/src/pty/pty.ts +25 -0
- package/src/pty/schema.ts +1 -0
- package/src/pty/ticket.ts +56 -0
- package/src/pty.ts +318 -0
- package/src/public-event-manifest.ts +7 -0
- package/src/question.ts +153 -0
- package/src/reference/guidance.ts +69 -0
- package/src/reference.ts +125 -0
- package/src/repository-cache.ts +259 -0
- package/src/repository.ts +214 -0
- package/src/ripgrep/binary.ts +132 -0
- package/src/ripgrep.ts +281 -0
- package/src/schema.ts +79 -0
- package/src/session/compaction.ts +241 -0
- package/src/session/context-epoch.ts +174 -0
- package/src/session/error.ts +24 -0
- package/src/session/event.ts +2 -0
- package/src/session/execution/local.ts +46 -0
- package/src/session/execution.ts +34 -0
- package/src/session/history.ts +101 -0
- package/src/session/info.ts +50 -0
- package/src/session/input.ts +288 -0
- package/src/session/message-updater.ts +397 -0
- package/src/session/message.ts +2 -0
- package/src/session/projector.ts +458 -0
- package/src/session/prompt.ts +1 -0
- package/src/session/revert.ts +121 -0
- package/src/session/run-coordinator.ts +104 -0
- package/src/session/runner/index.ts +28 -0
- package/src/session/runner/llm.ts +432 -0
- package/src/session/runner/max-steps.ts +16 -0
- package/src/session/runner/model.ts +218 -0
- package/src/session/runner/publish-llm-event.ts +423 -0
- package/src/session/runner/to-llm-message.ts +171 -0
- package/src/session/schema.ts +9 -0
- package/src/session/sql.ts +176 -0
- package/src/session/store.ts +63 -0
- package/src/session/todo.ts +78 -0
- package/src/session.ts +486 -0
- package/src/share/sql.ts +13 -0
- package/src/shell.ts +226 -0
- package/src/skill/discovery.ts +213 -0
- package/src/skill/guidance.ts +76 -0
- package/src/skill.ts +132 -0
- package/src/snapshot.ts +266 -0
- package/src/state.ts +128 -0
- package/src/system-context/builtins.ts +50 -0
- package/src/system-context/index.ts +320 -0
- package/src/system-context/registry.ts +49 -0
- package/src/tool/AGENTS.md +59 -0
- package/src/tool/application-tools.ts +57 -0
- package/src/tool/apply-patch.ts +219 -0
- package/src/tool/bash.ts +207 -0
- package/src/tool/builtins.ts +48 -0
- package/src/tool/edit.ts +223 -0
- package/src/tool/glob.ts +105 -0
- package/src/tool/grep.ts +137 -0
- package/src/tool/http-body.ts +30 -0
- package/src/tool/question.ts +94 -0
- package/src/tool/read-filesystem.ts +366 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/registry.ts +147 -0
- package/src/tool/skill.ts +109 -0
- package/src/tool/todowrite.ts +62 -0
- package/src/tool/tool.ts +162 -0
- package/src/tool/tools.ts +13 -0
- package/src/tool/webfetch.ts +218 -0
- package/src/tool/websearch.ts +260 -0
- package/src/tool/write.ts +101 -0
- package/src/tool-output-store.ts +211 -0
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/effect-flock.ts +284 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +70 -0
- package/src/util/flock.ts +358 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +11 -0
- package/src/util/identifier.ts +1 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/module.ts +10 -0
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +42 -0
- package/src/util/slug.ts +74 -0
- package/src/util/token.ts +5 -0
- package/src/util/which.ts +14 -0
- package/src/util/wildcard.ts +14 -0
- package/src/v1/config/agent.ts +89 -0
- package/src/v1/config/attachment.ts +25 -0
- package/src/v1/config/command.ts +13 -0
- package/src/v1/config/config.ts +192 -0
- package/src/v1/config/console-state.ts +16 -0
- package/src/v1/config/error.ts +39 -0
- package/src/v1/config/formatter.ts +13 -0
- package/src/v1/config/layout.ts +6 -0
- package/src/v1/config/lsp.ts +80 -0
- package/src/v1/config/mcp.ts +63 -0
- package/src/v1/config/migrate.ts +254 -0
- package/src/v1/config/permission.ts +50 -0
- package/src/v1/config/plugin.ts +9 -0
- package/src/v1/config/provider-options.ts +227 -0
- package/src/v1/config/provider.ts +121 -0
- package/src/v1/config/server.ts +19 -0
- package/src/v1/config/skills.ts +13 -0
- package/src/v1/permission.ts +33 -0
- package/src/v1/session.ts +68 -0
- package/src/v2-schema.ts +3 -0
- package/src/workspace.ts +6 -0
- package/sst-env.d.ts +10 -0
- package/test/agent.test.ts +131 -0
- package/test/application-tools.test.ts +287 -0
- package/test/background-job.test.ts +106 -0
- package/test/catalog.test.ts +353 -0
- package/test/command.test.ts +56 -0
- package/test/config/agent.test.ts +349 -0
- package/test/config/command.test.ts +83 -0
- package/test/config/config.test.ts +798 -0
- package/test/config/fixtures/plugin/directory-plugin.ts +13 -0
- package/test/config/plugin.test.ts +248 -0
- package/test/config/provider-options.test.ts +224 -0
- package/test/config/provider.test.ts +269 -0
- package/test/config/skill.test.ts +80 -0
- package/test/credential.test.ts +36 -0
- package/test/database-migration.test.ts +644 -0
- package/test/effect/cross-spawn-spawner.test.ts +426 -0
- package/test/effect/keyed-mutex.test.ts +73 -0
- package/test/effect/layer-node/layer-node-types.test.ts +143 -0
- package/test/effect/layer-node/layer-node.test.ts +261 -0
- package/test/effect/layer-node/node-build.test.ts +125 -0
- package/test/effect/observability.test.ts +109 -0
- package/test/event.test.ts +1124 -0
- package/test/file-mutation.test.ts +368 -0
- package/test/filesystem/filesystem.test.ts +387 -0
- package/test/filesystem/ignore.test.ts +10 -0
- package/test/filesystem/search.test.ts +44 -0
- package/test/filesystem/watcher.test.ts +266 -0
- package/test/fixture/effect-flock-worker.ts +60 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/git.ts +49 -0
- package/test/fixture/location.ts +26 -0
- package/test/fixture/tmpdir.ts +25 -0
- package/test/fixtures/recordings/session-runner/openai-chat-streams-text.json +27 -0
- package/test/git.test.ts +164 -0
- package/test/github-copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/github-copilot/copilot-chat-model.test.ts +592 -0
- package/test/github-copilot/openai-responses-language-model.test.ts +206 -0
- package/test/global.test.ts +16 -0
- package/test/instruction-context.test.ts +323 -0
- package/test/integration.test.ts +349 -0
- package/test/legacy-event-schema.test.ts +16 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/tool.ts +20 -0
- package/test/location-filesystem.test.ts +69 -0
- package/test/location-layer.test.ts +226 -0
- package/test/location-mutation.test.ts +178 -0
- package/test/location.test.ts +42 -0
- package/test/model.test.ts +23 -0
- package/test/models.test.ts +290 -0
- package/test/move-session.test.ts +235 -0
- package/test/npm-config.test.ts +51 -0
- package/test/npm.test.ts +84 -0
- package/test/oauth-page.test.ts +15 -0
- package/test/patch.test.ts +68 -0
- package/test/permission.test.ts +315 -0
- package/test/plugin/command.test.ts +47 -0
- package/test/plugin/fixture.ts +52 -0
- package/test/plugin/fixtures/config-effect-plugin.ts +15 -0
- package/test/plugin/fixtures/config-promise-plugin.ts +13 -0
- package/test/plugin/fixtures/invalid-plugin.ts +1 -0
- package/test/plugin/fixtures/models-dev.json +14 -0
- package/test/plugin/fixtures/provider-factory.ts +9 -0
- package/test/plugin/host.ts +303 -0
- package/test/plugin/models-dev.test.ts +170 -0
- package/test/plugin/promise.test.ts +67 -0
- package/test/plugin/provider-alibaba.test.ts +92 -0
- package/test/plugin/provider-amazon-bedrock.test.ts +622 -0
- package/test/plugin/provider-anthropic.test.ts +94 -0
- package/test/plugin/provider-azure-cognitive-services.test.ts +199 -0
- package/test/plugin/provider-azure.test.ts +293 -0
- package/test/plugin/provider-cerebras.test.ts +136 -0
- package/test/plugin/provider-cloudflare-ai-gateway.test.ts +426 -0
- package/test/plugin/provider-cloudflare-workers-ai.test.ts +279 -0
- package/test/plugin/provider-cohere.test.ts +123 -0
- package/test/plugin/provider-deepinfra.test.ts +159 -0
- package/test/plugin/provider-dynamic.test.ts +184 -0
- package/test/plugin/provider-gateway.test.ts +115 -0
- package/test/plugin/provider-github-copilot.test.ts +278 -0
- package/test/plugin/provider-gitlab.test.ts +308 -0
- package/test/plugin/provider-google-vertex-anthropic.test.ts +264 -0
- package/test/plugin/provider-google-vertex.test.ts +387 -0
- package/test/plugin/provider-google.test.ts +80 -0
- package/test/plugin/provider-groq.test.ts +121 -0
- package/test/plugin/provider-kilo.test.ts +99 -0
- package/test/plugin/provider-llmgateway.test.ts +80 -0
- package/test/plugin/provider-mistral.test.ts +129 -0
- package/test/plugin/provider-nvidia.test.ts +97 -0
- package/test/plugin/provider-openai-compatible.test.ts +107 -0
- package/test/plugin/provider-openai.test.ts +176 -0
- package/test/plugin/provider-opencode.test.ts +423 -0
- package/test/plugin/provider-openrouter.test.ts +113 -0
- package/test/plugin/provider-perplexity.test.ts +122 -0
- package/test/plugin/provider-sap-ai-core.test.ts +159 -0
- package/test/plugin/provider-snowflake-cortex.test.ts +261 -0
- package/test/plugin/provider-togetherai.test.ts +129 -0
- package/test/plugin/provider-venice.test.ts +115 -0
- package/test/plugin/provider-vercel.test.ts +83 -0
- package/test/plugin/provider-xai.test.ts +125 -0
- package/test/plugin/provider-zenmux.test.ts +110 -0
- package/test/plugin/skill.test.ts +25 -0
- package/test/plugin/variant.test.ts +67 -0
- package/test/plugin.test.ts +71 -0
- package/test/policy.test.ts +85 -0
- package/test/preload.ts +5 -0
- package/test/process/process.test.ts +368 -0
- package/test/project-copy.test.ts +400 -0
- package/test/project-directories.test.ts +61 -0
- package/test/project.test.ts +221 -0
- package/test/provider-mistral.test.ts +282 -0
- package/test/provider-xai-responses.test.ts +32 -0
- package/test/pty/info-schema.test.ts +32 -0
- package/test/pty/protocol.test.ts +27 -0
- package/test/pty/pty-session.test.ts +240 -0
- package/test/pty/ticket.test.ts +62 -0
- package/test/question.test.ts +114 -0
- package/test/reference-guidance.test.ts +76 -0
- package/test/reference.test.ts +78 -0
- package/test/repository-cache.test.ts +153 -0
- package/test/repository.test.ts +71 -0
- package/test/ripgrep.test.ts +65 -0
- package/test/session-compaction.test.ts +27 -0
- package/test/session-create.test.ts +425 -0
- package/test/session-history.test.ts +165 -0
- package/test/session-projector.test.ts +533 -0
- package/test/session-prompt.test.ts +584 -0
- package/test/session-run-coordinator.test.ts +418 -0
- package/test/session-runner-message.test.ts +501 -0
- package/test/session-runner-model.test.ts +347 -0
- package/test/session-runner-recorded.test.ts +193 -0
- package/test/session-runner-tool-events.test.ts +136 -0
- package/test/session-runner-tool-registry.test.ts +452 -0
- package/test/session-runner.test.ts +3365 -0
- package/test/session-todo.test.ts +94 -0
- package/test/session-tool-progress.test.ts +158 -0
- package/test/shared-schema.test.ts +206 -0
- package/test/shell.test.ts +108 -0
- package/test/skill/guidance.test.ts +144 -0
- package/test/skill-discovery.test.ts +165 -0
- package/test/skill.test.ts +125 -0
- package/test/snapshot.test.ts +178 -0
- package/test/state.test.ts +115 -0
- package/test/system-context/builtins.test.ts +129 -0
- package/test/system-context/index.test.ts +307 -0
- package/test/system-context/registry.test.ts +114 -0
- package/test/tool-apply-patch.test.ts +436 -0
- package/test/tool-bash.test.ts +437 -0
- package/test/tool-edit.test.ts +434 -0
- package/test/tool-output-store.test.ts +247 -0
- package/test/tool-question.test.ts +162 -0
- package/test/tool-read-filesystem.test.ts +118 -0
- package/test/tool-read.test.ts +674 -0
- package/test/tool-skill.test.ts +149 -0
- package/test/tool-todowrite.test.ts +125 -0
- package/test/tool-webfetch.test.ts +281 -0
- package/test/tool-websearch.test.ts +316 -0
- package/test/tool-write.test.ts +305 -0
- package/test/util/effect-flock.test.ts +392 -0
- package/test/util/flock.test.ts +428 -0
- package/test/util/which.test.ts +100 -0
- 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
|
+
}
|