Compare commits

..

22 Commits

Author SHA1 Message Date
opencode-agent[bot] 1dc54945d5 fix(tui): sync durable session model changes 2026-07-07 22:50:17 +00:00
Aiden Cline 3100701488 fix(tui): restore prompt context usage (#35795) 2026-07-07 17:49:33 -05:00
Kit Langton 9c54291ae8 fix(tui): restore forms on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:47:13 +00:00
Dax Raad 52ad916ba4 fix(tui): restore permissions on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:44:26 +00:00
Dax Raad 5db320b02d refactor: rename apply_patch tool 2026-07-07 16:27:25 -04:00
Aiden Cline 910e37f6d8 fix: update v2 session usage metrics (#35468) 2026-07-07 14:31:54 -05:00
Kit Langton 81f6e06681 feat: enable background subagents by default 2026-07-07 15:18:46 -04:00
James Long da68e2865e feat(tui): wire move session command (#35203) 2026-07-07 14:49:11 -04:00
Aiden Cline f86e1cc1ff fix(codemode): preserve collection value identity (#35776) 2026-07-07 13:40:55 -05:00
Aiden Cline 947bbf9490 feat(mcp): expose resource catalog API (#35773) 2026-07-07 13:34:29 -05:00
James Long 3e57b43a4a feat(session): support admit-only synthetic messages (#35774) 2026-07-07 14:17:20 -04:00
opencode-agent[bot] cb18a42a47 docs(codemode): clarify implicit return guidance (#35763)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 12:48:05 -05:00
Aiden Cline 1fb2837fd3 feat(codemode): expand numeric standard library (#35749) 2026-07-07 12:11:57 -05:00
Aiden Cline 66a8d830aa feat(core): support MCP resources (#35656) 2026-07-07 11:55:06 -05:00
opencode-agent[bot] d0733f83bf fix(codemode): return final top-level expression (#35759)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 11:44:50 -05:00
Kit Langton b2ce195bda fix(core): normalize Location.Ref keys in LocationServiceMap (#35757) 2026-07-07 12:25:29 -04:00
James Long 6c065ca6a5 feat(simulation): render driven UI screenshots and videos (#35739) 2026-07-07 12:15:01 -04:00
Dax Raad dd1c007877 docs 2026-07-07 12:08:45 -04:00
Kit Langton 8deb0e5780 fix(core): preserve SDK plugins across location eviction (#35725) 2026-07-07 11:43:40 -04:00
Aiden Cline f488089179 fix: preserve compaction files and classify Z.AI overflow (#35747)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
2026-07-07 09:54:31 -05:00
Kit Langton e3a39b214f fix(core): lower failed reasoning to text (#35735) 2026-07-07 09:49:17 -04:00
James Long b6a2912bb1 refactor(simulation): type RPC request payloads (#35734) 2026-07-07 09:21:19 -04:00
158 changed files with 3694 additions and 1275 deletions
+116 -6
View File
@@ -101,7 +101,6 @@
"dependencies": { "dependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@opencode-ai/client": "workspace:*", "@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
@@ -874,6 +873,7 @@
"name": "@opencode-ai/simulation", "name": "@opencode-ai/simulation",
"version": "1.17.13", "version": "1.17.13",
"dependencies": { "dependencies": {
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
@@ -1612,11 +1612,11 @@
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
@@ -1984,6 +1984,30 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@napi-rs/canvas": ["@napi-rs/canvas@1.0.2", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.2", "@napi-rs/canvas-darwin-arm64": "1.0.2", "@napi-rs/canvas-darwin-x64": "1.0.2", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", "@napi-rs/canvas-linux-arm64-musl": "1.0.2", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-musl": "1.0.2", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", "@napi-rs/canvas-win32-x64-msvc": "1.0.2" } }, "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ=="],
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g=="],
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ=="],
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A=="],
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ=="],
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA=="],
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA=="],
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw=="],
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g=="],
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q=="],
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ=="],
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
@@ -2278,7 +2302,7 @@
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="],
@@ -2570,7 +2594,37 @@
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="], "@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
@@ -3112,6 +3166,8 @@
"@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], "@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
@@ -3184,6 +3240,12 @@
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="],
"@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="],
"@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
@@ -4988,6 +5050,8 @@
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="], "opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="],
@@ -5402,6 +5466,8 @@
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="],
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
@@ -6022,8 +6088,12 @@
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -6620,6 +6690,10 @@
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -6640,6 +6714,8 @@
"@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
@@ -6748,6 +6824,14 @@
"@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="],
"@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="], "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="],
"@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], "@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
@@ -7044,6 +7128,8 @@
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], "p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -7708,6 +7794,10 @@
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
@@ -7716,6 +7806,8 @@
"@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
"@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], "@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
@@ -7786,6 +7878,22 @@
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="], "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
@@ -8352,6 +8460,8 @@
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -20,7 +20,7 @@ const profiles = [
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } }, { name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
{ {
name: "multi patch", name: "multi patch",
tool: "apply_patch", tool: "patch",
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] }, input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
}, },
] as const ] as const
@@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
userMessage(), userMessage(),
assistantMessage( assistantMessage(
[ [
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
textPart(followingID, "Following incremental patch"), textPart(followingID, "Following incremental patch"),
], ],
{ completed: false }, { completed: false },
@@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated( partUpdated(
toolPart( toolPart(
patchID, patchID,
"apply_patch", "patch",
"running", "running",
{ files: [first.filePath, second.filePath] }, { files: [first.filePath, second.filePath] },
{ metadata: { files: [first, second] } }, { metadata: { files: [first, second] } },
@@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated( partUpdated(
toolPart( toolPart(
patchID, patchID,
"apply_patch", "patch",
"completed", "completed",
{ files: [first.filePath, second.filePath, third.filePath] }, { files: [first.filePath, second.filePath, third.filePath] },
{ metadata: { files: [first, second, third] } }, { metadata: { files: [first, second, third] } },
@@ -295,7 +295,7 @@ function performanceTurn(index: number) {
messageID: assistantID, messageID: assistantID,
type: "tool", type: "tool",
callID: `call_0000_${suffix}_patch`, callID: `call_0000_${suffix}_patch`,
tool: "apply_patch", tool: "patch",
state: { state: {
status: "completed", status: "completed",
input: { patchText: realisticPatch(index) }, input: { patchText: realisticPatch(index) },
@@ -131,7 +131,7 @@ function toolPart(
): MessagePart { ): MessagePart {
const metadata = const metadata =
metadataOverride ?? metadataOverride ??
(tool === "apply_patch" (tool === "patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write" : tool === "edit" || tool === "write"
? { ? {
@@ -219,7 +219,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []), : []),
...(index % 8 === 0 ...(index % 8 === 0
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []), : []),
...(index % 7 === 0 ...(index % 7 === 0
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
@@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
assistantMessage([ assistantMessage([
toolPart( toolPart(
id, id,
"apply_patch", "patch",
"completed", "completed",
{ files: ["src/a.ts"] }, { files: ["src/a.ts"] },
{ {
@@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
assistantMessage([ assistantMessage([
toolPart( toolPart(
patchID, patchID,
"apply_patch", "patch",
"completed", "completed",
{ files: files.map((file) => file.filePath) }, { files: files.map((file) => file.filePath) },
{ metadata: { files } }, { metadata: { files } },
@@ -246,7 +246,7 @@ function editPart(id: string) {
function patchPart(id: string) { function patchPart(id: string) {
return toolPart( return toolPart(
id, id,
"apply_patch", "patch",
"completed", "completed",
{ files: ["src/a.ts", "src/b.ts"] }, { files: ["src/a.ts", "src/b.ts"] },
{ {
@@ -8,7 +8,7 @@ import {
} from "../performance/timeline-stability/fixture" } from "../performance/timeline-stability/fixture"
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => { test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const parts = ordinary.map((tool, index) => const parts = ordinary.map((tool, index) =>
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }), toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
) )
@@ -90,7 +90,7 @@ function questionInput() {
function errorInput(tool: string) { function errorInput(tool: string) {
if (tool === "bash") return { command: "exit 1" } if (tool === "bash") return { command: "exit 1" }
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" } if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
if (tool === "apply_patch") return { files: ["src/error.ts"] } if (tool === "patch") return { files: ["src/error.ts"] }
if (tool === "webfetch") return { url: "https://example.com" } if (tool === "webfetch") return { url: "https://example.com" }
if (tool === "websearch") return { query: "failure" } if (tool === "websearch") return { query: "failure" }
if (tool === "task") return { description: "Fail task", subagent_type: "explore" } if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
@@ -120,7 +120,7 @@ function toolPart(
outputLength = 160, outputLength = 160,
): MessagePart { ): MessagePart {
const metadata = const metadata =
tool === "apply_patch" tool === "patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write" : tool === "edit" || tool === "write"
? { ? {
@@ -199,7 +199,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []), : []),
...(index % 8 === 0 ...(index % 8 === 0
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] ? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []), : []),
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []), ...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []), ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
+1 -1
View File
@@ -55,7 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const path = url.pathname const path = url.pathname
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
if (path === "/global/health") return json(route, { healthy: true }) if (path === "/global/health") return json(route, { healthy: true })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false }) if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
if (path === "/permission") if (path === "/permission")
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
if (path === "/question") if (path === "/question")
@@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
const value = row() const value = row()
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID) const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool) return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
} }
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile()) const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
let contentMeasureFrame: number | undefined let contentMeasureFrame: number | undefined
Regular → Executable
View File
-1
View File
@@ -33,7 +33,6 @@
"dependencies": { "dependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@opencode-ai/client": "workspace:*", "@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
-66
View File
@@ -1,66 +0,0 @@
export * as CodeModeHost from "./code-mode"
import { NodeHttpClient } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { OpenAPI, Tool } from "@opencode-ai/codemode"
import { Api } from "@opencode-ai/server/api"
import { ServerAuth } from "@opencode-ai/server/auth"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi"
import type { Server } from "node:http"
export function replacements(server: Server, password: string): LayerNode.Replacements {
return [ToolRegistry.codeModeReplacement(makeTools(client(server), password))]
}
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
return {
opencode: bindTools(
OpenAPI.fromSpec({
spec: { ...OpenApi.fromApi(Api) },
baseUrl: "http://opencode.local",
headers: ServerAuth.headers({ username: "opencode", password }),
}).tools,
client,
),
}
}
function client(server: Server) {
return Layer.effect(
HttpClient.HttpClient,
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
return HttpClient.mapRequest(client, (request) => {
const address = server.address()
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
const local =
address.address === "0.0.0.0" ? "127.0.0.1" : address.address === "::" ? "::1" : address.address
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
const url = new URL(request.url)
return HttpClientRequest.setUrl(
request,
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
)
})
}),
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
}
function bindTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
return Object.fromEntries(
Object.entries(tools).map(([name, value]) => [
name,
Tool.isDefinition<HttpClient.HttpClient>(value)
? Tool.make({
description: value.description,
input: value.input,
output: value.output,
run: (input) => value.run(input).pipe(Effect.provide(client)),
})
: bindTools(value, client),
]),
)
}
+2 -2
View File
@@ -628,11 +628,11 @@ function emitEdit(state: State): void {
function emitPatch(state: State): void { function emitPatch(state: State): void {
const file = path.join(process.cwd(), "src", "demo-format.ts") const file = path.join(process.cwd(), "src", "demo-format.ts")
const ref = make(state, "apply_patch", { const ref = make(state, "patch", {
patchText: "*** Begin Patch\n*** End Patch", patchText: "*** Begin Patch\n*** End Patch",
}) })
doneTool(state, ref, { doneTool(state, ref, {
title: "apply_patch", title: "patch",
output: "", output: "",
metadata: { metadata: {
files: [ files: [
-2
View File
@@ -84,7 +84,6 @@ type RunFooterOptions = {
theme: RunTheme theme: RunTheme
keymap: Keymap<Renderable, KeyEvent> keymap: Keymap<Renderable, KeyEvent>
tuiConfig: RunTuiConfig tuiConfig: RunTuiConfig
backgroundSubagents: boolean
diffStyle: RunDiffStyle diffStyle: RunDiffStyle
onPermissionReply: (input: PermissionReply) => void | Promise<void> onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void> onQuestionReply: (input: QuestionReply) => void | Promise<void>
@@ -326,7 +325,6 @@ export class RunFooter implements FooterApi {
theme: footer.theme, theme: footer.theme,
diffStyle: options.diffStyle, diffStyle: options.diffStyle,
tuiConfig: options.tuiConfig, tuiConfig: options.tuiConfig,
backgroundSubagents: options.backgroundSubagents,
history: footer.history, history: footer.history,
agent: options.agentLabel, agent: options.agentLabel,
onSubmit: footer.handlePrompt, onSubmit: footer.handlePrompt,
+1 -4
View File
@@ -89,7 +89,6 @@ type RunFooterViewProps = {
theme: () => RunTheme theme: () => RunTheme
diffStyle?: RunDiffStyle diffStyle?: RunDiffStyle
tuiConfig: RunTuiConfig tuiConfig: RunTuiConfig
backgroundSubagents: boolean
history?: () => RunPrompt[] history?: () => RunPrompt[]
agent: string agent: string
onSubmit: (input: RunPrompt) => boolean onSubmit: (input: RunPrompt) => boolean
@@ -169,9 +168,7 @@ export function RunFooterView(props: RunFooterViewProps) {
return tabs().findIndex((item) => item.sessionID === sessionID) + 1 return tabs().findIndex((item) => item.sessionID === sessionID) + 1
}) })
const foregroundSubagents = createMemo( const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
() => props.backgroundSubagents && activeTabs().some((item) => !item.background),
)
const model = createMemo(() => { const model = createMemo(() => {
const current = props.currentModel() const current = props.currentModel()
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined } return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
-4
View File
@@ -1,6 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { truthy } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Effect } from "effect" import { Effect } from "effect"
import path from "node:path" import path from "node:path"
@@ -84,9 +83,6 @@ export async function runMini(input: MiniCommandInput) {
files: [], files: [],
initialInput, initialInput,
thinking: true, thinking: true,
backgroundSubagents:
truthy("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") ||
(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === undefined && truthy("OPENCODE_EXPERIMENTAL")),
replay: input.replay ?? true, replay: input.replay ?? true,
replayLimit: input.replayLimit, replayLimit: input.replayLimit,
demo: input.demo, demo: input.demo,
@@ -64,7 +64,6 @@ export type LifecycleInput = {
model: RunInput["model"] model: RunInput["model"]
variant: string | undefined variant: string | undefined
tuiConfig: RunTuiConfig | Promise<RunTuiConfig> tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
backgroundSubagents: boolean
onPermissionReply: (input: PermissionReply) => void | Promise<void> onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void> onQuestionReply: (input: QuestionReply) => void | Promise<void>
onQuestionReject: (input: QuestionReject) => void | Promise<void> onQuestionReject: (input: QuestionReject) => void | Promise<void>
@@ -236,7 +235,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
wrote, wrote,
keymap, keymap,
tuiConfig, tuiConfig,
backgroundSubagents: input.backgroundSubagents,
diffStyle: tuiConfig.diff_style ?? "auto", diffStyle: tuiConfig.diff_style ?? "auto",
onPermissionReply: input.onPermissionReply, onPermissionReply: input.onPermissionReply,
onQuestionReply: input.onQuestionReply, onQuestionReply: input.onQuestionReply,
-5
View File
@@ -56,7 +56,6 @@ type RunRuntimeInput = {
files: RunInput["files"] files: RunInput["files"]
initialInput?: string initialInput?: string
thinking: boolean thinking: boolean
backgroundSubagents: boolean
replay?: boolean replay?: boolean
replayLimit?: number replayLimit?: number
demo?: RunInput["demo"] demo?: RunInput["demo"]
@@ -75,7 +74,6 @@ type RunDeferredInput = {
files: RunInput["files"] files: RunInput["files"]
initialInput?: string initialInput?: string
thinking: boolean thinking: boolean
backgroundSubagents: boolean
replay?: boolean replay?: boolean
replayLimit?: number replayLimit?: number
demo?: RunInput["demo"] demo?: RunInput["demo"]
@@ -270,7 +268,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
model: state.model, model: state.model,
variant: state.activeVariant, variant: state.activeVariant,
tuiConfig: tuiConfigTask, tuiConfig: tuiConfigTask,
backgroundSubagents: input.backgroundSubagents,
onPermissionReply: async (next) => { onPermissionReply: async (next) => {
if (state.demo?.permission(next)) { if (state.demo?.permission(next)) {
return return
@@ -876,7 +873,6 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
files: input.files, files: input.files,
initialInput: input.initialInput, initialInput: input.initialInput,
thinking: input.thinking, thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay, replay: input.replay,
replayLimit: input.replayLimit, replayLimit: input.replayLimit,
demo: input.demo, demo: input.demo,
@@ -928,7 +924,6 @@ export async function runInteractiveMode(
files: input.files, files: input.files,
initialInput: input.initialInput, initialInput: input.initialInput,
thinking: input.thinking, thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay, replay: input.replay,
replayLimit: input.replayLimit, replayLimit: input.replayLimit,
demo: input.demo, demo: input.demo,
+2 -2
View File
@@ -119,7 +119,7 @@ type ToolName =
| "bash" | "bash"
| "write" | "write"
| "edit" | "edit"
| "apply_patch" | "patch"
| "batch" | "batch"
| "task" | "task"
| "todowrite" | "todowrite"
@@ -1094,7 +1094,7 @@ const TOOL_RULES = {
}, },
permission: permEdit, permission: permEdit,
}, },
apply_patch: { patch: {
view: { view: {
output: false, output: false,
final: true, final: true,
-1
View File
@@ -135,7 +135,6 @@ export type RunInput = {
files: RunFilePart[] files: RunFilePart[]
initialInput?: string initialInput?: string
thinking: boolean thinking: boolean
backgroundSubagents: boolean
demo?: boolean demo?: boolean
} }
-11
View File
@@ -7,13 +7,11 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import { Flock } from "@opencode-ai/core/util/flock"
import { start } from "@opencode-ai/server/process" import { start } from "@opencode-ai/server/process"
import { randomBytes, randomUUID } from "node:crypto" import { randomBytes, randomUUID } from "node:crypto"
import path from "node:path" import path from "node:path"
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect" import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
import { HttpServer } from "effect/unstable/http" import { HttpServer } from "effect/unstable/http"
import { CodeModeHost } from "./code-mode"
import { Env } from "./env" import { Env } from "./env"
import { ServiceConfig } from "./services/service-config" import { ServiceConfig } from "./services/service-config"
import { Updater } from "./services/updater" import { Updater } from "./services/updater"
@@ -38,14 +36,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home)) if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
return yield* Effect.scoped( return yield* Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
if (options.mode === "service") {
const service = yield* ServiceConfig.options()
yield* Flock.effect(path.basename(service.file, ".json") + "-process", {
dir: path.dirname(service.file),
staleMs: 3_000,
timeoutMs: 15_000,
})
}
const environmentPassword = yield* Env.password const environmentPassword = yield* Env.password
// Keep the lease credential out of the environment inherited by tools. // Keep the lease credential out of the environment inherited by tools.
if (options.mode === "stdio") { if (options.mode === "stdio") {
@@ -64,7 +54,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
hostname: options.hostname ?? config.hostname ?? "127.0.0.1", hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
port: Option.fromNullishOr(options.port ?? config.port), port: Option.fromNullishOr(options.port ?? config.port),
password, password,
replacements: (server) => CodeModeHost.replacements(server, password),
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false }))) }).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
if (options.mode === "service") yield* register(address, password) if (options.mode === "service") yield* register(address, password)
const url = HttpServer.formatAddress(address) const url = HttpServer.formatAddress(address)
-39
View File
@@ -1,39 +0,0 @@
import { expect, test } from "bun:test"
import { CodeMode } from "@opencode-ai/codemode"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { CodeModeHost } from "../src/code-mode"
test("exposes the authenticated OpenCode API through CodeMode", async () => {
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
const client = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push({ url: request.url, authorization: request.headers.authorization })
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json(
{ healthy: true, version: "test", pid: 1 },
{ headers: { "content-type": "application/json" } },
),
),
)
}),
)
const result = await CodeMode.make({ tools: CodeModeHost.makeTools(client, "secret") })
.execute("return await tools.opencode.v2.health.get({})")
.pipe(Effect.runPromise)
expect(result).toEqual({
ok: true,
value: { healthy: true, version: "test", pid: 1 },
toolCalls: [{ name: "opencode.v2.health.get" }],
})
expect(requests).toEqual([
{
url: "http://opencode.local/api/health",
authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
},
])
})
+115 -92
View File
@@ -111,157 +111,167 @@ export type Endpoint4_8Input = {
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>> export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E> export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0] type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
export type Endpoint4_9Input = { export type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"] readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"] readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"] readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
} }
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"] export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
export type SessionPromptOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E> export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0] type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
export type Endpoint4_10Input = { export type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"] readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"] readonly id?: Endpoint4_10Request["payload"]["id"]
readonly command: Endpoint4_10Request["payload"]["command"] readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"] readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"] readonly resume?: Endpoint4_10Request["payload"]["resume"]
} }
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"] export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E> export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0] type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
export type Endpoint4_11Input = { export type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"] readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"] readonly id?: Endpoint4_11Request["payload"]["id"]
readonly skill: Endpoint4_11Request["payload"]["skill"] readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly resume?: Endpoint4_11Request["payload"]["resume"] readonly resume?: Endpoint4_11Request["payload"]["resume"]
} }
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>> export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionSkillOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E> export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0] type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
export type Endpoint4_12Input = { export type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"] readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly text: Endpoint4_12Request["payload"]["text"] readonly id?: Endpoint4_12Request["payload"]["id"]
readonly description?: Endpoint4_12Request["payload"]["description"] readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"] readonly resume?: Endpoint4_12Request["payload"]["resume"]
} }
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>> export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E> export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0] type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
export type Endpoint4_13Input = { export type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"] readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly id?: Endpoint4_13Request["payload"]["id"] readonly text: Endpoint4_13Request["payload"]["text"]
readonly command: Endpoint4_13Request["payload"]["command"] readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
} }
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>> export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionShellOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E> export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0] type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
export type Endpoint4_14Input = { export type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"] readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"] readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
} }
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"] export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionCompactOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E> export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0] type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] } export type Endpoint4_15Input = {
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>> readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
export type SessionWaitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E> readonly id?: Endpoint4_15Request["payload"]["id"]
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_16Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"]
} }
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"] export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E> export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0] type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] } export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>> export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E> export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0] type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_17Request["payload"]["files"]
}
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] } export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>> export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E> export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0] type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] } export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"] export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionContextOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E> export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0] type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] } export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
export type Endpoint4_20Output = EffectValue< export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
export type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
export type Endpoint4_21Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]> ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
>["data"] >["data"]
export type SessionInstructionsEntryListOperation<E = never> = ( export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint4_20Input,
) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"]
}
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint4_21Input, input: Endpoint4_21Input,
) => Effect.Effect<Endpoint4_21Output, E> ) => Effect.Effect<Endpoint4_21Output, E>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0] type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint4_22Input = { export type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"] readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"] readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_22Request["payload"]["value"]
} }
export type Endpoint4_22Output = EffectValue< export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]> export type SessionInstructionsEntryPutOperation<E = never> = (
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint4_22Input, input: Endpoint4_22Input,
) => Effect.Effect<Endpoint4_22Output, E> ) => Effect.Effect<Endpoint4_22Output, E>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0] type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
export type Endpoint4_23Input = { export type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"] readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"] readonly key: Endpoint4_23Request["params"]["key"]
readonly follow?: Endpoint4_23Request["query"]["follow"]
} }
export type Endpoint4_23Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>> export type Endpoint4_23Output = EffectValue<
export type SessionLogOperation<E = never> = (input: Endpoint4_23Input) => Stream.Stream<Endpoint4_23Output, E> ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint4_23Input,
) => Effect.Effect<Endpoint4_23Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0] type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] } export type Endpoint4_24Input = {
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>> readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
export type SessionInterruptOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E> readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_24Request["query"]["follow"]
}
export type Endpoint4_24Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint4_24Input) => Stream.Stream<Endpoint4_24Output, E>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0] type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] } export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>> export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E> export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0] type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_26Input = { export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
readonly sessionID: Endpoint4_26Request["params"]["sessionID"] export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
readonly messageID: Endpoint4_26Request["params"]["messageID"] export type SessionBackgroundOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_27Input = {
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_27Request["params"]["messageID"]
} }
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"] export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E> export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
export interface SessionApi<E = never> { export interface SessionApi<E = never> {
readonly list: SessionListOperation<E> readonly list: SessionListOperation<E>
@@ -273,6 +283,7 @@ export interface SessionApi<E = never> {
readonly switchAgent: SessionSwitchAgentOperation<E> readonly switchAgent: SessionSwitchAgentOperation<E>
readonly switchModel: SessionSwitchModelOperation<E> readonly switchModel: SessionSwitchModelOperation<E>
readonly rename: SessionRenameOperation<E> readonly rename: SessionRenameOperation<E>
readonly move: SessionMoveOperation<E>
readonly prompt: SessionPromptOperation<E> readonly prompt: SessionPromptOperation<E>
readonly command: SessionCommandOperation<E> readonly command: SessionCommandOperation<E>
readonly skill: SessionSkillOperation<E> readonly skill: SessionSkillOperation<E>
@@ -439,8 +450,14 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>> export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E> export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
export type ServerMcpCatalogOperation<E = never> = (input?: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
export interface ServerMcpApi<E = never> { export interface ServerMcpApi<E = never> {
readonly list: ServerMcpListOperation<E> readonly list: ServerMcpListOperation<E>
readonly catalog: ServerMcpCatalogOperation<E>
} }
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0] type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
@@ -890,8 +907,14 @@ export interface VcsApi<E = never> {
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>> export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E> export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export interface DebugApi<E = never> { export interface DebugApi<E = never> {
readonly location: DebugLocationOperation<E> readonly location: DebugLocationOperation<E>
readonly evictLocation: DebugEvictLocationOperation<E>
} }
export interface AppApi<E = never> { export interface AppApi<E = never> {
+144 -112
View File
@@ -141,15 +141,27 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
) )
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0] type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint4_9Input = { type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"] readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"] readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"] readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
} }
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) => const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
raw["session.move"]({
params: { sessionID: input["sessionID"] },
payload: { destination: input["destination"], moveChanges: input["moveChanges"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
raw["session.prompt"]({ raw["session.prompt"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] }, payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
@@ -158,20 +170,20 @@ const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Inp
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0] type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_10Input = { type Endpoint4_11Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"] readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"] readonly id?: Endpoint4_11Request["payload"]["id"]
readonly command: Endpoint4_10Request["payload"]["command"] readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"] readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"] readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"] readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"] readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"] readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"] readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"] readonly resume?: Endpoint4_11Request["payload"]["resume"]
} }
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) => const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
raw["session.command"]({ raw["session.command"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { payload: {
@@ -190,67 +202,73 @@ const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10I
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0] type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_11Input = { type Endpoint4_12Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"] readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"] readonly id?: Endpoint4_12Request["payload"]["id"]
readonly skill: Endpoint4_11Request["payload"]["skill"] readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly resume?: Endpoint4_11Request["payload"]["resume"] readonly resume?: Endpoint4_12Request["payload"]["resume"]
} }
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) => const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.skill"]({ raw["session.skill"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0] type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly text: Endpoint4_12Request["payload"]["text"]
readonly description?: Endpoint4_12Request["payload"]["description"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
}
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_13Input = { type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"] readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly id?: Endpoint4_13Request["payload"]["id"] readonly text: Endpoint4_13Request["payload"]["text"]
readonly command: Endpoint4_13Request["payload"]["command"] readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
} }
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) => const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
}
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.shell"]({ raw["session.shell"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { id: input["id"], command: input["command"] }, payload: { id: input["id"], command: input["command"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0] type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_14Input = { type Endpoint4_15Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"] readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"] readonly id?: Endpoint4_15Request["payload"]["id"]
} }
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) => const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0] type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] } type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) => const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0] type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_16Input = { type Endpoint4_17Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"] readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"] readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"] readonly files?: Endpoint4_17Request["payload"]["files"]
} }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) => const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.revert.stage"]({ raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] }, payload: { messageID: input["messageID"], files: input["files"] },
@@ -259,61 +277,61 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0] type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] } type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) => const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0] type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] } type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) => const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0] type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] } type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) => const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
) )
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0] type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_21Input = { type Endpoint4_22Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"] readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"] readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"] readonly value: Endpoint4_22Request["payload"]["value"]
} }
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) => const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.instructions.entry.put"]({ raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] }, params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] }, payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0] type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_22Input = { type Endpoint4_23Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"] readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"] readonly key: Endpoint4_23Request["params"]["key"]
} }
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) => const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
) )
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0] type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_23Input = { type Endpoint4_24Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"] readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"] readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_23Request["query"]["follow"] readonly follow?: Endpoint4_24Request["query"]["follow"]
} }
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) => const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
Stream.unwrap( Stream.unwrap(
raw["session.log"]({ raw["session.log"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
@@ -324,22 +342,22 @@ const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23I
), ),
) )
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0] type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] } type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) => const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0] type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_26Input = { type Endpoint4_27Input = {
readonly sessionID: Endpoint4_26Request["params"]["sessionID"] readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_26Request["params"]["messageID"] readonly messageID: Endpoint4_27Request["params"]["messageID"]
} }
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) => const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
@@ -355,22 +373,23 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
switchAgent: Endpoint4_6(raw), switchAgent: Endpoint4_6(raw),
switchModel: Endpoint4_7(raw), switchModel: Endpoint4_7(raw),
rename: Endpoint4_8(raw), rename: Endpoint4_8(raw),
prompt: Endpoint4_9(raw), move: Endpoint4_9(raw),
command: Endpoint4_10(raw), prompt: Endpoint4_10(raw),
skill: Endpoint4_11(raw), command: Endpoint4_11(raw),
synthetic: Endpoint4_12(raw), skill: Endpoint4_12(raw),
shell: Endpoint4_13(raw), synthetic: Endpoint4_13(raw),
compact: Endpoint4_14(raw), shell: Endpoint4_14(raw),
wait: Endpoint4_15(raw), compact: Endpoint4_15(raw),
revertStage: Endpoint4_16(raw), wait: Endpoint4_16(raw),
revertClear: Endpoint4_17(raw), revertStage: Endpoint4_17(raw),
revertCommit: Endpoint4_18(raw), revertClear: Endpoint4_18(raw),
context: Endpoint4_19(raw), revertCommit: Endpoint4_19(raw),
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } }, context: Endpoint4_20(raw),
log: Endpoint4_23(raw), instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
interrupt: Endpoint4_24(raw), log: Endpoint4_24(raw),
background: Endpoint4_25(raw), interrupt: Endpoint4_25(raw),
message: Endpoint4_26(raw), background: Endpoint4_26(raw),
message: Endpoint4_27(raw),
}) })
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0] type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
@@ -529,7 +548,12 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) => const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) }) type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw), catalog: Endpoint10_1(raw) })
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0] type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
type Endpoint11_0Input = { type Endpoint11_0Input = {
@@ -1069,7 +1093,15 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () => const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)) raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) }) type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
location: Endpoint25_0(raw),
evictLocation: Endpoint25_1(raw),
})
const adaptClient = (raw: RawClient) => ({ const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]), health: adaptGroup0(raw["server.health"]),
@@ -23,6 +23,8 @@ import type {
SessionSwitchModelOutput, SessionSwitchModelOutput,
SessionRenameInput, SessionRenameInput,
SessionRenameOutput, SessionRenameOutput,
SessionMoveInput,
SessionMoveOutput,
SessionPromptInput, SessionPromptInput,
SessionPromptOutput, SessionPromptOutput,
SessionCommandInput, SessionCommandInput,
@@ -87,6 +89,8 @@ import type {
IntegrationAttemptCancelOutput, IntegrationAttemptCancelOutput,
ServerMcpListInput, ServerMcpListInput,
ServerMcpListOutput, ServerMcpListOutput,
ServerMcpCatalogInput,
ServerMcpCatalogOutput,
CredentialUpdateInput, CredentialUpdateInput,
CredentialUpdateOutput, CredentialUpdateOutput,
CredentialRemoveInput, CredentialRemoveInput,
@@ -178,6 +182,8 @@ import type {
VcsDiffInput, VcsDiffInput,
VcsDiffOutput, VcsDiffOutput,
DebugLocationOutput, DebugLocationOutput,
DebugEvictLocationInput,
DebugEvictLocationOutput,
} from "./types" } from "./types"
import { ClientError } from "./client-error" import { ClientError } from "./client-error"
@@ -485,6 +491,18 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
request<SessionMoveOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
body: { destination: input["destination"], moveChanges: input["moveChanges"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) => prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionPromptOutput }>( request<{ readonly data: SessionPromptOutput }>(
{ {
@@ -536,7 +554,12 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`, path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
body: { text: input["text"], description: input["description"], metadata: input["metadata"] }, body: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
successStatus: 204, successStatus: 204,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: true, empty: true,
@@ -890,6 +913,18 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
catalog: (input?: ServerMcpCatalogInput, requestOptions?: RequestOptions) =>
request<ServerMcpCatalogOutput>(
{
method: "GET",
path: `/api/mcp/resource`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
}, },
credential: { credential: {
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) => update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
@@ -1494,6 +1529,18 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
request<DebugEvictLocationOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
}, },
} }
} }
@@ -526,6 +526,20 @@ export type SessionRenameInput = {
export type SessionRenameOutput = void export type SessionRenameOutput = void
export type SessionMoveInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly destination: {
readonly destination: { readonly directory: string }
readonly moveChanges?: boolean | undefined
}["destination"]
readonly moveChanges?: {
readonly destination: { readonly directory: string }
readonly moveChanges?: boolean | undefined
}["moveChanges"]
}
export type SessionMoveOutput = void
export type SessionPromptInput = { export type SessionPromptInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"] readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: { readonly id?: {
@@ -856,17 +870,26 @@ export type SessionSyntheticInput = {
readonly text: string readonly text: string
readonly description?: string | null readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly resume?: boolean | null
}["text"] }["text"]
readonly description?: { readonly description?: {
readonly text: string readonly text: string
readonly description?: string | null readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly resume?: boolean | null
}["description"] }["description"]
readonly metadata?: { readonly metadata?: {
readonly text: string readonly text: string
readonly description?: string | null readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue } readonly metadata?: { readonly [x: string]: JsonValue }
readonly resume?: boolean | null
}["metadata"] }["metadata"]
readonly resume?: {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly resume?: boolean | null
}["resume"]
} }
export type SessionSyntheticOutput = void export type SessionSyntheticOutput = void
@@ -1416,6 +1439,13 @@ export type SessionLogOutput =
readonly sessionID: string readonly sessionID: string
readonly assistantMessageID: string readonly assistantMessageID: string
readonly error: { readonly type: string; readonly message: string } readonly error: { readonly type: string; readonly message: string }
readonly cost?: number
readonly tokens?: {
readonly input: number
readonly output: number
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
} }
} }
| { | {
@@ -2478,6 +2508,36 @@ export type ServerMcpListOutput = {
}> }>
} }
export type ServerMcpCatalogInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type ServerMcpCatalogOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: {
readonly resources: ReadonlyArray<{
readonly server: string
readonly name: string
readonly uri: string
readonly description?: string
readonly mimeType?: string
}>
readonly templates: ReadonlyArray<{
readonly server: string
readonly name: string
readonly uriTemplate: string
readonly description?: string
readonly mimeType?: string
}>
}
}
export type CredentialUpdateInput = { export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"] readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: {
@@ -4489,6 +4549,23 @@ export type EventSubscribeOutput =
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string } readonly data: { readonly sessionID: string; readonly title: string }
} }
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.usage.updated"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly cost: number
readonly tokens: {
readonly input: number
readonly output: number
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
}
}
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
@@ -4712,6 +4789,13 @@ export type EventSubscribeOutput =
readonly sessionID: string readonly sessionID: string
readonly assistantMessageID: string readonly assistantMessageID: string
readonly error: { readonly type: string; readonly message: string } readonly error: { readonly type: string; readonly message: string }
readonly cost?: number
readonly tokens?: {
readonly input: number
readonly output: number
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
} }
} }
| { | {
@@ -5508,6 +5592,14 @@ export type EventSubscribeOutput =
readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly server: string } readonly data: { readonly server: string }
} }
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "mcp.resources.changed"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly server: string }
}
| { | {
readonly id: string readonly id: string
readonly created: number readonly created: number
@@ -6130,3 +6222,11 @@ export type VcsDiffOutput = {
} }
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }> export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
export type DebugEvictLocationInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type DebugEvictLocationOutput = void
+24 -1
View File
@@ -32,7 +32,7 @@ test("exposes every standard HTTP API group", () => {
"vcs", "vcs",
"debug", "debug",
]) ])
expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug)).toEqual(["location", "evictLocation"])
expect(Object.keys(client.message)).toEqual(["list"]) expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual([ expect(Object.keys(client.integration)).toEqual([
"list", "list",
@@ -50,6 +50,29 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
}) })
test("MCP resource catalog uses the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input) => {
request = input instanceof Request ? input : new Request(input)
return Response.json({
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
data: {
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
},
})
},
})
const result = await client["server.mcp"].catalog({ location: { directory: "/tmp/project" } })
expect(result.data.resources[0]?.uri).toBe("docs://readme")
expect(request?.method).toBe("GET")
expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
})
test("file.read returns binary content from the public HTTP contract", async () => { test("file.read returns binary content from the public HTTP contract", async () => {
let request: Request | undefined let request: Request | undefined
const client = OpenCode.make({ const client = OpenCode.make({
+2 -2
View File
@@ -62,7 +62,7 @@ const result =
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption. `result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well. Successful result values are JSON-safe data. An explicit `return` produces the program result; when it is omitted, the final executable top-level expression is returned as a model-friendly REPL convenience. Otherwise reaching the end produces `null`. Returned `undefined` and nested `undefined` values are normalized to `null` as well.
## API ## API
@@ -241,7 +241,7 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports:
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`. - `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring. - Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`. - Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`. - Common array, string, number, `Object`, `Math`, and `JSON` operations, including primitive-number `valueOf`, the standard non-finite `Number` constants, and host-backed `Math.random`. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones). - `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout. - Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators). - `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
+1 -1
View File
@@ -158,7 +158,7 @@ current omissions to implement, not intentional product boundaries.
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics. - [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set - [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
composition methods, and `Array.prototype.toSpliced`. composition methods, and `Array.prototype.toSpliced`.
- [ ] Decide whether nondeterministic `Math.random` and iterable `Math.sumPrecise` belong in the runtime. - [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter - [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
defects are distinguishable without leaking private causes. defects are distinguishable without leaking private causes.
@@ -30,7 +30,6 @@ export type Binding = {
export type StatementResult = export type StatementResult =
| { kind: "none" } | { kind: "none" }
| { kind: "value"; value: unknown }
| { kind: "return"; value: unknown } | { kind: "return"; value: unknown }
| { kind: "break" } | { kind: "break" }
| { kind: "continue" } | { kind: "continue" }
+24 -37
View File
@@ -530,17 +530,29 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
if (args[0] instanceof SandboxURLSearchParams) { if (args[0] instanceof SandboxURLSearchParams) {
return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
} }
const source = boundedData(args[0], "Array.from input") const source = args[0]
if (source instanceof SandboxPromise) {
throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.",
node,
"InvalidDataValue",
)
}
if (typeof source === "string") return Array.from(source) if (typeof source === "string") return Array.from(source)
if (Array.isArray(source)) return [...source] if (Array.isArray(source)) return [...source]
if ( if (
source !== null && source !== null &&
typeof source === "object" && typeof source === "object" &&
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
typeof (source as { length?: unknown }).length === "number" typeof (source as { length?: unknown }).length === "number"
) { ) {
return Array.from(source as ArrayLike<unknown>) return Array.from(source as ArrayLike<unknown>)
} }
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node) throw new InterpreterRuntimeError(
"Array.from expects an array, string, Map, Set, or array-like value.",
node,
"InvalidDataValue",
)
} }
default: default:
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node) throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
@@ -606,7 +618,6 @@ class Interpreter<R> {
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself. // ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string> private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
private readonly logs: Array<string> private readonly logs: Array<string>
private lastValue: unknown
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap). // Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
private readonly callPermits: Semaphore.Semaphore private readonly callPermits: Semaphore.Semaphore
// Fiber-backed promises whose settlement no program construct has observed yet. Successful // Fiber-backed promises whose settlement no program construct has observed yet. Successful
@@ -625,7 +636,6 @@ class Interpreter<R> {
this.invokeTool = invokeTool this.invokeTool = invokeTool
this.toolKeys = toolKeys this.toolKeys = toolKeys
this.logs = logs this.logs = logs
this.lastValue = undefined
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY) this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>() this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
@@ -671,13 +681,15 @@ class Interpreter<R> {
return Effect.gen(function* () { return Effect.gen(function* () {
self.hoistFunctions(program.body) self.hoistFunctions(program.body)
let value: unknown = undefined let value: unknown = undefined
let returned = false for (const [index, statement] of program.body.entries()) {
for (const statement of program.body) { if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
value = yield* self.evaluateExpression(getNode(statement, "expression"))
break
}
const result = yield* self.evaluateStatement(statement) const result = yield* self.evaluateStatement(statement)
if (result.kind === "return") { if (result.kind === "return") {
value = result.value value = result.value
returned = true
break break
} }
@@ -685,11 +697,7 @@ class Interpreter<R> {
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement) throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
} }
if (result.kind === "value") {
self.lastValue = result.value
} }
}
if (!returned) value = self.lastValue
// The program body runs inside an implicit async function, so a returned promise // The program body runs inside an implicit async function, so a returned promise
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works // resolves before crossing the data boundary - `return tools.ns.tool(...)` works
@@ -780,7 +788,7 @@ class Interpreter<R> {
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> { private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
switch (node.type) { switch (node.type) {
case "ExpressionStatement": case "ExpressionStatement":
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value })) return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" })
case "VariableDeclaration": case "VariableDeclaration":
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
case "ReturnStatement": { case "ReturnStatement": {
@@ -833,11 +841,6 @@ class Interpreter<R> {
const statement = asNode(statementValue, "body") const statement = asNode(statementValue, "body")
const result = yield* self.evaluateStatement(statement) const result = yield* self.evaluateStatement(statement)
if (result.kind === "value") {
self.lastValue = result.value
continue
}
if (result.kind !== "none") { if (result.kind !== "none") {
return result return result
} }
@@ -929,7 +932,6 @@ class Interpreter<R> {
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
if (result.kind === "break") return { kind: "none" } satisfies StatementResult if (result.kind === "break") return { kind: "none" } satisfies StatementResult
if (result.kind === "return" || result.kind === "continue") return result if (result.kind === "return" || result.kind === "continue") return result
if (result.kind === "value") self.lastValue = result.value
} }
} }
return { kind: "none" } satisfies StatementResult return { kind: "none" } satisfies StatementResult
@@ -957,9 +959,6 @@ class Interpreter<R> {
return result return result
} }
if (result.kind === "value") {
self.lastValue = result.value
}
} }
return { kind: "none" } satisfies StatementResult return { kind: "none" } satisfies StatementResult
@@ -987,9 +986,6 @@ class Interpreter<R> {
return result return result
} }
if (result.kind === "value") {
self.lastValue = result.value
}
} while (yield* self.evaluateExpression(testNode)) } while (yield* self.evaluateExpression(testNode))
return { kind: "none" } satisfies StatementResult return { kind: "none" } satisfies StatementResult
@@ -1045,10 +1041,6 @@ class Interpreter<R> {
return { kind: "none" } satisfies StatementResult return { kind: "none" } satisfies StatementResult
} }
if (result.kind === "value") {
self.lastValue = result.value
}
if (iterationScope) { if (iterationScope) {
const loopScope = self.currentScope() const loopScope = self.currentScope()
for (const name of perIterationBindings) { for (const name of perIterationBindings) {
@@ -1133,10 +1125,6 @@ class Interpreter<R> {
return { kind: "none" } return { kind: "none" }
} }
if (result.kind === "value") {
self.lastValue = result.value
}
if (result.kind === "continue") { if (result.kind === "continue") {
continue continue
} }
@@ -1226,10 +1214,6 @@ class Interpreter<R> {
return { kind: "none" } return { kind: "none" }
} }
if (result.kind === "value") {
self.lastValue = result.value
}
if (result.kind === "continue") { if (result.kind === "continue") {
continue continue
} }
@@ -2033,6 +2017,9 @@ class Interpreter<R> {
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) { if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
return invokeGlobalMethod(callable, args, node) return invokeGlobalMethod(callable, args, node)
} }
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
return invokeGlobalMethod(callable, args, node)
}
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
} }
if (callable instanceof CoercionFunction) { if (callable instanceof CoercionFunction) {
@@ -2372,7 +2359,7 @@ class Interpreter<R> {
if (fn.body.type === "BlockStatement") { if (fn.body.type === "BlockStatement") {
const result = yield* invocation.evaluateStatement(fn.body) const result = yield* invocation.evaluateStatement(fn.body)
return result.kind === "return" || result.kind === "value" ? result.value : undefined return result.kind === "return" ? result.value : undefined
} }
return yield* invocation.evaluateExpression(fn.body) return yield* invocation.evaluateExpression(fn.body)
+2
View File
@@ -1,6 +1,7 @@
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"]) export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
export const mathMethods = new Set([ export const mathMethods = new Set([
"random",
"max", "max",
"min", "min",
"abs", "abs",
@@ -40,6 +41,7 @@ export const mathMethods = new Set([
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => { export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node) if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
if (name === "random") return Math.random()
const nums = args.map((arg) => { const nums = args.map((arg) => {
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
return arg return arg
+14 -2
View File
@@ -1,6 +1,15 @@
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"]) export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"]) export const numberConstants = new Set([
"MAX_SAFE_INTEGER",
"MIN_SAFE_INTEGER",
"MAX_VALUE",
"MIN_VALUE",
"EPSILON",
"NaN",
"POSITIVE_INFINITY",
"NEGATIVE_INFINITY",
])
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"]) export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
@@ -32,6 +41,9 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
result = value.toString(radix) result = value.toString(radix)
break break
} }
case "valueOf":
result = value
break
default: default:
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node) throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
} }
+18 -15
View File
@@ -1,6 +1,6 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember } from "../tool-runtime.js" import { isBlockedMember } from "../tool-runtime.js"
import { isSandboxValue, SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js" import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js" import { boundedData, coerceToString } from "./value.js"
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"]) export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
@@ -10,13 +10,23 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node) if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
const requireObject = (): Record<string, unknown> => { const requireObject = (): Record<string, unknown> => {
const input = args[0] const input = args[0]
const value = boundedData(args[0], `Object.${name} input`)
if (Array.isArray(input)) return input as unknown as Record<string, unknown> if (Array.isArray(input)) return input as unknown as Record<string, unknown>
if (isSandboxValue(value)) return {} if (isSandboxValue(input)) return {}
if (value === null || typeof value !== "object") { if (input instanceof SandboxPromise) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node) throw new InterpreterRuntimeError(
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
node,
"InvalidDataValue",
)
} }
return value as Record<string, unknown> if (input === null || typeof input !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
const prototype = Object.getPrototypeOf(input)
if (prototype !== null && prototype !== Object.prototype) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
return input as Record<string, unknown>
} }
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => { const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node) if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
@@ -28,15 +38,8 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
guardedSet(out, coerceToString(key), item) guardedSet(out, coerceToString(key), item)
} }
switch (name) { switch (name) {
case "keys": { case "keys":
const value = boundedData(args[0], "Object.keys input") return Object.keys(requireObject())
if (isSandboxValue(value)) return []
if (Array.isArray(value)) return Object.keys(value)
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
}
return Object.keys(value)
}
case "values": case "values":
return Object.values(requireObject()) return Object.values(requireObject())
case "entries": case "entries":
+1
View File
@@ -617,6 +617,7 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
"", "",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.", "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.", "Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
] ]
+21
View File
@@ -658,6 +658,9 @@ describe("CodeMode public contract", () => {
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers") expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
expect(instructions).not.toContain("host globals") expect(instructions).not.toContain("host globals")
expect(instructions).toContain("Use Code Mode tools for external operations") expect(instructions).toContain("Use Code Mode tools for external operations")
expect(instructions).toContain(
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
)
expect(instructions).toContain( expect(instructions).toContain(
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.", "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
) )
@@ -1081,6 +1084,24 @@ describe("CodeMode public contract", () => {
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
}) })
test("returns the final top-level expression when return is omitted", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` }))
expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] })
})
test("does not implicitly return expressions nested in control flow", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` }))
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
})
test("returns null when the final top-level statement is not an expression", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` }))
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
})
test("rejects invalid configuration and discovery limits", async () => { test("rejects invalid configuration and discovery limits", async () => {
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError) expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow( expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
+6
View File
@@ -245,6 +245,12 @@ describe("promises at data boundaries", () => {
expect(diagnostic.message).toContain("await tools.ns.tool(...)") expect(diagnostic.message).toContain("await tools.ns.tool(...)")
}) })
test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("un-awaited Promise")
})
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => { test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`) const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.kind).toBe("InvalidDataValue")
+106
View File
@@ -19,6 +19,28 @@ const error = async (code: string) => {
return result.error return result.error
} }
describe("Number and Math", () => {
test("Math.random returns a number in [0, 1)", async () => {
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
})
test("Number exposes native non-finite constants", async () => {
expect(
await value(
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
),
).toEqual([true, true, true])
})
test("Number valueOf returns its primitive receiver", async () => {
expect(await value(`return (42).valueOf()`)).toBe(42)
})
test("Number valueOf does not enable boxed numbers", async () => {
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
})
})
describe("Date", () => { describe("Date", () => {
test("Date.now() returns a number", async () => { test("Date.now() returns a number", async () => {
expect(await value(`return typeof Date.now()`)).toBe("number") expect(await value(`return typeof Date.now()`)).toBe("number")
@@ -751,6 +773,43 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
) )
}) })
test("Object.values/entries preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.values(rows)[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.entries(rows)[0][1].selected = true
return child.selected
`),
).toBe(true)
})
test("Object enumeration preserves promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
const source = { pending }
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
`),
).toEqual([["pending"], true, 1, 1])
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
})
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
})
test("Object.assign keeps Maps usable", async () => { test("Object.assign keeps Maps usable", async () => {
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe( expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
1, 1,
@@ -773,6 +832,53 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5) expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
}) })
test("Array.from and Array.of preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
Array.from([child])[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
Array.of(child)[0].selected = true
return child.selected
`),
).toBe(true)
})
test("Array.from and Array.of preserve promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
return [await Array.from([pending])[0], await Array.of(pending)[0]]
`),
).toEqual([1, 1])
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
})
test("Array.from preserves identity across supported collection shapes", async () => {
expect(
await value(`
const child = { selected: false }
const fromArrayLike = Array.from({ 0: child, length: 1 })
const fromMap = Array.from(new Map([["child", child]]))
const fromSet = Array.from(new Set([child]))
fromArrayLike[0].selected = true
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
`),
).toEqual([true, true, true])
})
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
})
test("regexes stay callable through Object.values", async () => { test("regexes stay callable through Object.values", async () => {
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true) expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
}) })
+3 -2
View File
@@ -6,6 +6,7 @@ import type {
JSONValue, JSONValue,
LanguageModelV3, LanguageModelV3,
LanguageModelV3CallOptions, LanguageModelV3CallOptions,
LanguageModelV3FinishReason,
LanguageModelV3FunctionTool, LanguageModelV3FunctionTool,
LanguageModelV3Message, LanguageModelV3Message,
LanguageModelV3Prompt, LanguageModelV3Prompt,
@@ -624,8 +625,8 @@ function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["us
return Object.values(output).some((value) => value !== undefined) ? output : undefined return Object.values(output).some((value) => value !== undefined) ? output : undefined
} }
function finishReason(value: unknown): FinishReason { function finishReason(value: LanguageModelV3FinishReason): FinishReason {
return Schema.is(FinishReason)(value) ? value : "unknown" return value.unified === "other" ? "unknown" : value.unified
} }
function providerMetadata(value: unknown) { function providerMetadata(value: unknown) {
+1 -1
View File
@@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
// TODO: Publish watcher/file-edit events after V2 watcher integration exists. // TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists. // TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. // TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits. // TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// Until then, edits are sequential and report partial application. // Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. // TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
+13
View File
@@ -150,8 +150,14 @@ export type LocationError = LayerNode.Error<typeof locationServices>
export function buildLocationServiceMap( export function buildLocationServiceMap(
replacements: LayerNode.Replacements = [], replacements: LayerNode.Replacements = [],
): Layer.Layer<LocationServiceMap.Service> { ): Layer.Layer<LocationServiceMap.Service> {
// Structural Equal is own-key-set sensitive, so `{ directory }` (schema-decoded
// payloads omit optional keys) and `{ directory, workspaceID: undefined }` are
// different RcMap keys. The RcMap caches by the raw key before the build
// callback runs, so canonicalize at the map boundary to the key-present shape.
const canonical = (ref: Location.Ref) => Location.Ref.make({ directory: ref.directory, workspaceID: ref.workspaceID })
return Layer.effect( return Layer.effect(
LocationServiceMap.Service, LocationServiceMap.Service,
Effect.map(
LayerMap.make( LayerMap.make(
(ref: Location.Ref) => { (ref: Location.Ref) => {
const startedAt = performance.now() const startedAt = performance.now()
@@ -176,6 +182,13 @@ export function buildLocationServiceMap(
}, },
{ idleTimeToLive: "60 minutes" }, { idleTimeToLive: "60 minutes" },
), ),
(inner) => ({
...inner,
get: (ref: Location.Ref) => inner.get(canonical(ref)),
contextEffect: (ref: Location.Ref) => inner.contextEffect(canonical(ref)),
invalidate: (ref: Location.Ref) => inner.invalidate(canonical(ref)),
}),
),
) )
} }
+115 -13
View File
@@ -3,7 +3,7 @@ export * as MCPClient from "./client"
import path from "node:path" import path from "node:path"
import { execFile } from "node:child_process" import { execFile } from "node:child_process"
import { pathToFileURL } from "node:url" import { pathToFileURL } from "node:url"
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js" import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
@@ -21,6 +21,7 @@ import {
ListToolsResultSchema, ListToolsResultSchema,
PromptListChangedNotificationSchema, PromptListChangedNotificationSchema,
PromptSchema, PromptSchema,
ResourceListChangedNotificationSchema,
type LoggingMessageNotification, type LoggingMessageNotification,
LoggingMessageNotificationSchema, LoggingMessageNotificationSchema,
ToolListChangedNotificationSchema, ToolListChangedNotificationSchema,
@@ -68,11 +69,13 @@ export interface ToolDefinition {
export interface PromptDefinition { export interface PromptDefinition {
readonly name: string readonly name: string
readonly description: string | undefined readonly description: string | undefined
readonly arguments: ReadonlyArray<{ readonly arguments:
| ReadonlyArray<{
readonly name: string readonly name: string
readonly description: string | undefined readonly description: string | undefined
readonly required: boolean | undefined readonly required: boolean | undefined
}> | undefined }>
| undefined
} }
export interface PromptMessage { export interface PromptMessage {
@@ -84,6 +87,28 @@ export interface PromptResult {
readonly messages: ReadonlyArray<PromptMessage> readonly messages: ReadonlyArray<PromptMessage>
} }
export interface ResourceDefinition {
readonly name: string
readonly uri: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export interface ResourceTemplateDefinition {
readonly name: string
readonly uriTemplate: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export type ResourceContentPart =
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType: string | undefined }
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType: string | undefined }
export interface ReadResourceResult {
readonly contents: ReadonlyArray<ResourceContentPart>
}
export type CallToolContent = export type CallToolContent =
| { readonly type: "text"; readonly text: string } | { readonly type: "text"; readonly text: string }
| { readonly type: "media"; readonly data: string; readonly mimeType: string } | { readonly type: "media"; readonly data: string; readonly mimeType: string }
@@ -124,6 +149,12 @@ export interface Connection {
readonly tools: () => Effect.Effect<ToolDefinition[], Error> readonly tools: () => Effect.Effect<ToolDefinition[], Error>
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */ /** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
readonly prompts: () => Effect.Effect<PromptDefinition[], Error> readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
/** Lists the server's resources; returns [] when the server doesn't advertise resource support. */
readonly resources: () => Effect.Effect<ResourceDefinition[], Error>
/** Lists the server's resource templates; returns [] when the server doesn't advertise resource support. */
readonly resourceTemplates: () => Effect.Effect<ResourceTemplateDefinition[], Error>
/** Reads one resource; returns undefined when the server doesn't advertise resource support. */
readonly readResource: (input: { readonly uri: string }) => Effect.Effect<ReadResourceResult | undefined, Error>
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */ /** Invokes a prompt on the server. Interruption aborts the in-flight request. */
readonly prompt: (input: { readonly prompt: (input: {
readonly name: string readonly name: string
@@ -141,6 +172,8 @@ export interface Connection {
readonly onToolsChanged: (callback: () => void) => void readonly onToolsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */ /** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
readonly onPromptsChanged: (callback: () => void) => void readonly onPromptsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its resource catalog changed. */
readonly onResourcesChanged: (callback: () => void) => void
} }
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */ /** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
@@ -168,7 +201,8 @@ export const connect = Effect.fnUntraced(function* (
}, },
}) })
} }
if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` }) if (!URL.canParse(config.url))
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
return new StreamableHTTPClientTransport(new URL(config.url), { return new StreamableHTTPClientTransport(new URL(config.url), {
requestInit: config.headers ? { headers: config.headers } : undefined, requestInit: config.headers ? { headers: config.headers } : undefined,
authProvider, authProvider,
@@ -202,10 +236,7 @@ export const connect = Effect.fnUntraced(function* (
}).pipe(Effect.exit) }).pipe(Effect.exit)
if (Exit.isSuccess(exit)) { if (Exit.isSuccess(exit)) {
yield* Effect.addFinalizer(() => yield* Effect.addFinalizer(() =>
cleanupStdioDescendants(transport).pipe( cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
Effect.andThen(Effect.promise(() => client.close())),
Effect.ignore,
),
) )
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
@@ -257,7 +288,9 @@ export const connect = Effect.fnUntraced(function* (
), ),
catch: (error) => (error instanceof Error ? error : new Error(String(error))), catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe( }).pipe(
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })), Effect.tapError((error) =>
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
),
) )
return prompts.map((prompt) => ({ return prompts.map((prompt) => ({
name: prompt.name, name: prompt.name,
@@ -269,6 +302,74 @@ export const connect = Effect.fnUntraced(function* (
})), })),
})) }))
}), }),
resources: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const resources = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
(result) => result.resources,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
),
)
return resources.map((resource) => ({
name: resource.name,
uri: resource.uri,
description: resource.description,
mimeType: resource.mimeType,
}))
}),
resourceTemplates: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const templates = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, {
timeout: catalogTimeout,
}),
(result) => result.resourceTemplates,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
),
)
return templates.map((template) => ({
name: template.name,
uriTemplate: template.uriTemplate,
description: template.description,
mimeType: template.mimeType,
}))
}),
readResource: (input) =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return undefined
const result = yield* Effect.tryPromise({
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
),
)
return {
contents: result.contents.map(
(part): ResourceContentPart =>
"text" in part
? { type: "text", uri: part.uri, text: part.text, mimeType: part.mimeType }
: { type: "blob", uri: part.uri, blob: part.blob, mimeType: part.mimeType },
),
}
}),
prompt: (input) => prompt: (input) =>
Effect.tryPromise({ Effect.tryPromise({
try: (signal) => try: (signal) =>
@@ -328,13 +429,14 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.prompts?.listChanged) return if (!client.getServerCapabilities()?.prompts?.listChanged) return
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback()) client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
}, },
onResourcesChanged: (callback) => {
if (!client.getServerCapabilities()?.resources?.listChanged) return
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
},
} satisfies Connection } satisfies Connection
} }
yield* cleanupStdioDescendants(transport).pipe( yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
Effect.andThen(Effect.promise(() => transport.close())),
Effect.ignore,
)
const error = Cause.squash(exit.cause) const error = Cause.squash(exit.cause)
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server }) if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) }) return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
+80 -50
View File
@@ -83,48 +83,16 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
messages: Schema.Array(PromptMessage), messages: Schema.Array(PromptMessage),
}) {} }) {}
export class Resource extends Schema.Class<Resource>("MCP.Resource")({ export const Resource = Mcp.Resource
server: ServerName, export type Resource = Mcp.Resource
name: Schema.String, export const ResourceTemplate = Mcp.ResourceTemplate
uri: Schema.String, export type ResourceTemplate = Mcp.ResourceTemplate
description: Schema.String.pipe(Schema.optional), export const ResourceCatalog = Mcp.ResourceCatalog
mimeType: Schema.String.pipe(Schema.optional), export type ResourceCatalog = Mcp.ResourceCatalog
}) {} export const ResourceContentPart = Mcp.ResourceContentPart
export type ResourceContentPart = Mcp.ResourceContentPart
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({ export const ResourceContent = Mcp.ResourceContent
server: ServerName, export type ResourceContent = Mcp.ResourceContent
name: Schema.String,
uriTemplate: Schema.String,
description: Schema.String.pipe(Schema.optional),
mimeType: Schema.String.pipe(Schema.optional),
}) {}
export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}) {}
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
]).pipe(Schema.toTaggedUnion("type"))
export type ResourceContentPart = typeof ResourceContentPart.Type
export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
server: ServerName,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}) {}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", { export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
server: ServerName, server: ServerName,
@@ -415,6 +383,24 @@ export const layer = Layer.effect(
), ),
}) })
const toResource = (server: ServerName, def: MCPClient.ResourceDefinition) =>
Resource.make({
server,
name: def.name,
uri: def.uri,
description: def.description,
mimeType: def.mimeType,
})
const toResourceTemplate = (server: ServerName, def: MCPClient.ResourceTemplateDefinition) =>
ResourceTemplate.make({
server,
name: def.name,
uriTemplate: def.uriTemplate,
description: def.description,
mimeType: def.mimeType,
})
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
connection.tools().pipe( connection.tools().pipe(
Effect.map((defs) => { Effect.map((defs) => {
@@ -443,6 +429,7 @@ export const layer = Layer.effect(
entry.prompts = undefined entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" } entry.status = { status: "failed", error: "Connection closed" }
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)) fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)) fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)) fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
}) })
@@ -458,6 +445,10 @@ export const layer = Layer.effect(
connection.onPromptsChanged(() => { connection.onPromptsChanged(() => {
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore)) fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
}) })
connection.onResourcesChanged(() => {
if (entry.client !== connection) return
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
})
} }
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => { const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@@ -501,6 +492,7 @@ export const layer = Layer.effect(
// after the initial registration sweep and emits no list-changed notification would otherwise // after the initial registration sweep and emits no list-changed notification would otherwise
// stay invisible to the model. // stay invisible to the model.
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore)) fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
return return
@@ -557,11 +549,6 @@ export const layer = Layer.effect(
concurrency: "unbounded", concurrency: "unbounded",
discard: true, discard: true,
}) })
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
const target = yield* requireServer(server)
yield* Deferred.await(target.entry.startup)
})
return Service.of({ return Service.of({
servers: Effect.fn("MCP.servers")(function* () { servers: Effect.fn("MCP.servers")(function* () {
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b)) const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
@@ -637,11 +624,54 @@ export const layer = Layer.effect(
}), }),
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () { resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
yield* whenAllReady yield* whenAllReady
return new ResourceCatalog({ resources: [], templates: [] }) const catalogs = yield* Effect.forEach(
Array.from(runtime),
([name, entry]) => {
if (!entry.client) return Effect.succeed({ resources: [], templates: [] })
return Effect.all(
{
resources: entry.client.resources().pipe(Effect.catch(() => Effect.succeed([]))),
templates: entry.client.resourceTemplates().pipe(Effect.catch(() => Effect.succeed([]))),
},
{ concurrency: "unbounded" },
).pipe(
Effect.map((catalog) => ({
resources: catalog.resources.map((def) => toResource(name, def)),
templates: catalog.templates.map((def) => toResourceTemplate(name, def)),
})),
)
},
{ concurrency: "unbounded" },
)
return ResourceCatalog.make({
resources: catalogs
.flatMap((catalog) => catalog.resources)
.toSorted(
(a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name) || a.uri.localeCompare(b.uri),
),
templates: catalogs
.flatMap((catalog) => catalog.templates)
.toSorted(
(a, b) =>
a.server.localeCompare(b.server) ||
a.name.localeCompare(b.name) ||
a.uriTemplate.localeCompare(b.uriTemplate),
),
})
}), }),
readResource: Effect.fn("MCP.readResource")(function* (input) { readResource: Effect.fn("MCP.readResource")(function* (input) {
yield* gate(input.server) const target = yield* requireServer(input.server)
return undefined yield* Deferred.await(target.entry.startup)
if (!target.entry.client) return undefined
const result = yield* target.entry.client
.readResource({ uri: input.uri })
.pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!result) return undefined
return ResourceContent.make({
server: target.name,
uri: input.uri,
contents: result.contents,
})
}), }),
}) })
}), }),
+8 -24
View File
@@ -7,14 +7,6 @@ import { EventV2 } from "../event"
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} }) export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
export interface Store {
readonly plugins: Map<string, Plugin>
}
export const makeStore = (): Store => ({ plugins: new Map() })
const defaultStore = makeStore()
/** /**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes, * Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
* so `PluginSupervisor` can add them on every Location boot through the ordinary * so `PluginSupervisor` can add them on every Location boot through the ordinary
@@ -22,10 +14,9 @@ const defaultStore = makeStore()
* config. Registration publishes an unlocated update so every booted Location * config. Registration publishes an unlocated update so every booted Location
* reloads its plugin generation from the shared store. * reloads its plugin generation from the shared store.
* *
* The store is shared explicitly between the SDK construction graph and the * Each host-global layer owns one private store. Location graphs reuse that
* embedded route graph because `LocationServiceMap` builds Location layers lazily * layer through Effect's memoization, so separate hosts remain isolated while
* in a nested graph. Each embedded SDK creates its own store, so instances do not * every Location in one host sees the same registrations.
* see each other's contributions.
*/ */
export interface Interface { export interface Interface {
readonly register: (plugin: Plugin) => Effect.Effect<void> readonly register: (plugin: Plugin) => Effect.Effect<void>
@@ -34,26 +25,19 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {} export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
export const layerWithStore = (store: Store) => export const layer = Layer.effect(
Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
yield* Effect.addFinalizer(() => const plugins = new Map<string, Plugin>()
Effect.sync(() => {
store.plugins.clear()
}),
)
return Service.of({ return Service.of({
register: (plugin) => register: (plugin) =>
Effect.sync(() => { Effect.sync(() => {
store.plugins.set(plugin.id, plugin) plugins.set(plugin.id, plugin)
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid), }).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
all: () => [...store.plugins.values()], all: () => [...plugins.values()],
}) })
}), }),
) )
export const layer = layerWithStore(defaultStore)
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] }) export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
+2
View File
@@ -243,6 +243,7 @@ export interface Interface {
text: string text: string
description?: string description?: string
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
resume?: boolean
}) => Effect.Effect<void, NotFoundError> }) => Effect.Effect<void, NotFoundError>
readonly revert: { readonly revert: {
readonly stage: (input: { readonly stage: (input: {
@@ -682,6 +683,7 @@ const layer = Layer.effect(
description: input.description, description: input.description,
metadata: input.metadata, metadata: input.metadata,
}) })
if (input.resume === false) return
yield* execution yield* execution
.resume(input.sessionID) .resume(input.sessionID)
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
+11 -4
View File
@@ -25,20 +25,27 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"] - [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
## Work State ## Work State
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"] ### Completed
- Active: [current work, partial changes, or investigation state; otherwise "(none)"] - [finished work, verified facts, or changes made; otherwise "(none)"]
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
### Active
- [current work, partial changes, or investigation state; otherwise "(none)"]
### Blocked
- [blockers, failing commands, or unknowns; otherwise "(none)"]
## Next Move ## Next Move
1. [immediate concrete action, or "(none)"] 1. [immediate concrete action, or "(none)"]
2. [next action if known, or "(none)"] 2. [next action if known, or "(none)"]
## Relevant Files
- [file or directory path: why it matters, or "(none)"]
</template> </template>
Rules: Rules:
- Keep every section, even when empty. - Keep every section, even when empty.
- Use terse bullets, not prose paragraphs. - Use terse bullets, not prose paragraphs.
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known. - Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
- Put relevant files and symbols inside the section where they matter; do not add extra sections.
- Do not mention the summary process or that context was compacted.` - Do not mention the summary process or that context was compacted.`
type Settings = { type Settings = {
@@ -143,6 +143,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* SessionEvent.All.match(event, { yield* SessionEvent.All.match(event, {
"session.usage.updated": () => Effect.void,
"session.agent.selected": (event) => { "session.agent.selected": (event) => {
return adapter.appendMessage( return adapter.appendMessage(
SessionMessage.AgentSelected.make({ SessionMessage.AgentSelected.make({
@@ -296,6 +297,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
draft.finish = "error" draft.finish = "error"
draft.error = castDraft(event.data.error) draft.error = castDraft(event.data.error)
draft.retry = undefined draft.retry = undefined
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
draft.cost = event.data.cost
draft.tokens = castDraft(event.data.tokens)
}
}) })
}, },
"session.text.started": (event) => { "session.text.started": (event) => {
+60 -47
View File
@@ -1,7 +1,7 @@
export * as SessionProjector from "./projector" export * as SessionProjector from "./projector"
import { and, asc, desc, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm" import { and, asc, desc, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema } from "effect" import { DateTime, Effect, Layer, Schema, Stream } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/app-node"
@@ -48,11 +48,6 @@ type Usage = {
const ForkBatchSize = 500 const ForkBatchSize = 500
const emptyUsage = (): Usage => ({
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
const forkTitle = (value: string) => { const forkTitle = (value: string) => {
const match = value.match(/^(.+) \(fork #(\d+)\)$/) const match = value.match(/^(.+) \(fork #(\d+)\)$/)
if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})` if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})`
@@ -67,22 +62,6 @@ function usage(part: (typeof SessionV1.Event.PartUpdated.Type)["data"]["part"] |
return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] } return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] }
} }
function addUsage(target: Usage, value: Usage) {
target.cost += value.cost
target.tokens.input += value.tokens.input
target.tokens.output += value.tokens.output
target.tokens.reasoning += value.tokens.reasoning
target.tokens.cache.read += value.tokens.cache.read
target.tokens.cache.write += value.tokens.cache.write
}
function messageUsage(row: typeof SessionMessageTable.$inferSelect): Usage | undefined {
if (row.type !== "assistant") return undefined
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
if (message.type !== "assistant" || message.cost === undefined || message.tokens === undefined) return undefined
return { cost: message.cost, tokens: message.tokens }
}
function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInsert { function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInsert {
return { return {
id: info.id, id: info.id,
@@ -151,6 +130,37 @@ function applyUsage(
.pipe(Effect.orDie) .pipe(Effect.orDie)
} }
const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* (
db: DatabaseService,
events: EventV2.Interface,
sessionID: (typeof SessionEvent.Step.Ended.Type)["data"]["sessionID"],
) {
const row = yield* db
.select({
cost: SessionTable.cost,
input: SessionTable.tokens_input,
output: SessionTable.tokens_output,
reasoning: SessionTable.tokens_reasoning,
cacheRead: SessionTable.tokens_cache_read,
cacheWrite: SessionTable.tokens_cache_write,
})
.from(SessionTable)
.where(eq(SessionTable.id, sessionID))
.get()
.pipe(Effect.orDie)
if (!row) return
yield* events.publish(SessionEvent.UsageUpdated, {
sessionID,
cost: row.cost,
tokens: {
input: row.input,
output: row.output,
reasoning: row.reasoning,
cache: { read: row.cacheRead, write: row.cacheWrite },
},
})
})
const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
db: DatabaseService, db: DatabaseService,
event: typeof SessionEvent.Forked.Type, event: typeof SessionEvent.Forked.Type,
@@ -187,7 +197,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.limit(1) .limit(1)
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const copiedSeq = copied?.seq ?? 0 const copiedSeq = copied?.seq
const stored = yield* db const stored = yield* db
.insert(SessionTable) .insert(SessionTable)
@@ -237,9 +247,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.pipe(Effect.orDie) .pipe(Effect.orDie)
} }
const usage = emptyUsage()
let cursor = -1 let cursor = -1
while (true) { while (copiedSeq !== undefined) {
const rows = yield* db const rows = yield* db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
@@ -247,7 +256,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
and( and(
eq(SessionMessageTable.session_id, event.data.parentID), eq(SessionMessageTable.session_id, event.data.parentID),
gt(SessionMessageTable.seq, cursor), gt(SessionMessageTable.seq, cursor),
copiedSeq === 0 ? undefined : lt(SessionMessageTable.seq, copiedSeq + 1), lt(SessionMessageTable.seq, copiedSeq + 1),
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`, sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`,
), ),
) )
@@ -318,27 +327,9 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.pipe(Effect.orDie) .pipe(Effect.orDie)
} }
for (const row of rows) {
const value = messageUsage(row)
if (value) addUsage(usage, value)
}
cursor = rows.at(-1)!.seq cursor = rows.at(-1)!.seq
} }
if (copiedSeq !== undefined) yield* EventV2.reserveSequence(db, event.data.sessionID, copiedSeq)
yield* db
.update(SessionTable)
.set({
cost: usage.cost,
tokens_input: usage.tokens.input,
tokens_output: usage.tokens.output,
tokens_reasoning: usage.tokens.reasoning,
tokens_cache_read: usage.tokens.cache.read,
tokens_cache_write: usage.tokens.cache.write,
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
if (copiedSeq > 0) yield* EventV2.reserveSequence(db, event.data.sessionID, copiedSeq)
}) })
function run(db: DatabaseService, event: MessageEvent) { function run(db: DatabaseService, event: MessageEvent) {
@@ -697,8 +688,19 @@ const layer = Layer.effectDiscard(
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Step.Started, (event) => run(db, event))
yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Step.Ended, (event) =>
yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event)) Effect.gen(function* () {
yield* run(db, event)
yield* applyUsage(db, event.data.sessionID, event.data)
}),
)
yield* events.project(SessionEvent.Step.Failed, (event) =>
Effect.gen(function* () {
yield* run(db, event)
if (event.data.cost !== undefined && event.data.tokens !== undefined)
yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens })
}),
)
yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Text.Started, (event) => run(db, event))
yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event))
yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
@@ -790,6 +792,17 @@ const layer = Layer.effectDiscard(
yield* InstructionCheckpoint.reset(db, event.data.sessionID) yield* InstructionCheckpoint.reset(db, event.data.sessionID)
}), }),
) )
yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed]).pipe(
Stream.runForEach((event) => {
if (
event.type === SessionEvent.Step.Failed.type &&
(event.data.cost === undefined || event.data.tokens === undefined)
)
return Effect.void
return publishSessionUsage(db, events, event.data.sessionID)
}),
Effect.forkScoped({ startImmediately: true }),
)
}), }),
) )
+33 -3
View File
@@ -17,6 +17,7 @@ import { Config } from "../../config"
import { Database } from "../../database/database" import { Database } from "../../database/database"
import { EventV2 } from "../../event" import { EventV2 } from "../../event"
import { Location } from "../../location" import { Location } from "../../location"
import { ModelV2 } from "../../model"
import { PermissionV2 } from "../../permission" import { PermissionV2 } from "../../permission"
import { Instructions } from "../../instructions/index" import { Instructions } from "../../instructions/index"
import { InstructionBuiltIns } from "../../instructions/builtins" import { InstructionBuiltIns } from "../../instructions/builtins"
@@ -50,6 +51,30 @@ import { StepFailedError, UserInterruptedError } from "../error"
import { toSessionError } from "../to-session-error" import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry" import { SessionRunnerRetry } from "./retry"
type StepTokens = {
readonly input: number
readonly output: number
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
// TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract.
export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) {
const context = tokens.input + tokens.cache.read + tokens.cache.write
const tier = costs
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
.toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0]
const cost = tier ?? costs.find((cost) => cost.tier === undefined)
if (!cost) return 0
return (
(tokens.input * cost.input +
(tokens.output + tokens.reasoning) * cost.output +
tokens.cache.read * cost.cache.read +
tokens.cache.write * cost.cache.write) /
1_000_000
)
}
/** /**
* Runs one durable coding-agent Session until it settles. * Runs one durable coding-agent Session until it settles.
* *
@@ -312,6 +337,11 @@ const layer = Layer.effect(
Effect.ensuring(serialized(publisher.flush())), Effect.ensuring(serialized(publisher.flush())),
) )
const stepUsage = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => ({
cost: calculateCost(resolved.cost, settlement.tokens),
tokens: settlement.tokens,
})
// Captures the end snapshot, diffs it against the step's start, and durably ends the // Captures the end snapshot, diffs it against the step's start, and durably ends the
// assistant step. // assistant step.
const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
@@ -328,8 +358,7 @@ const layer = Layer.effect(
sessionID: session.id, sessionID: session.id,
assistantMessageID: yield* publisher.startAssistant(), assistantMessageID: yield* publisher.startAssistant(),
finish: settlement.finish, finish: settlement.finish,
cost: 0, ...stepUsage(settlement),
tokens: settlement.tokens,
snapshot: endSnapshot, snapshot: endSnapshot,
files, files,
}), }),
@@ -452,7 +481,8 @@ const layer = Layer.effect(
const stepEndedCleanly = const stepEndedCleanly =
!streamInterrupted && !toolsInterrupted && infraError === undefined && !providerFailed && !stepFailure !streamInterrupted && !toolsInterrupted && infraError === undefined && !providerFailed && !stepFailure
if (stepSettlement && stepEndedCleanly) yield* publishStepEnd(stepSettlement) if (stepSettlement && stepEndedCleanly) yield* publishStepEnd(stepSettlement)
if (stepFailure) yield* serialized(publisher.publishStepFailure()) if (stepFailure)
yield* serialized(publisher.publishStepFailure(stepSettlement ? stepUsage(stepSettlement) : undefined))
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (userDeclined) return yield* Effect.interrupt if (userDeclined) return yield* Effect.interrupt
+5 -1
View File
@@ -82,6 +82,8 @@ export interface Resolved {
readonly model: Model readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */ /** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref readonly ref: ModelV2.Ref
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
} }
export interface Interface { export interface Interface {
@@ -94,13 +96,14 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve })) export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */ /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved => ({ export const resolved = (model: Model, variant?: ModelV2.VariantID, cost: ModelV2.Info["cost"] = []): Resolved => ({
model, model,
ref: ModelV2.Ref.make({ ref: ModelV2.Ref.make({
id: ModelV2.ID.make(model.id), id: ModelV2.ID.make(model.id),
providerID: ProviderV2.ID.make(model.provider), providerID: ProviderV2.ID.make(model.provider),
...(variant === undefined ? {} : { variant }), ...(variant === undefined ? {} : { variant }),
}), }),
cost,
}) })
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
@@ -341,6 +344,7 @@ const layer = Layer.effect(
providerID: selected.providerID, providerID: selected.providerID,
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }), ...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
}), }),
cost: selected.cost,
} }
}), }),
}) })
@@ -216,7 +216,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
if (replace || stepFailure === undefined) stepFailure = error if (replace || stepFailure === undefined) stepFailure = error
}) })
const publishStepFailure = Effect.fnUntraced(function* () { const publishStepFailure = Effect.fnUntraced(function* (usage?: {
readonly cost: number
readonly tokens: ReturnType<typeof tokens>
}) {
if (stepFailed || stepFailure === undefined) return if (stepFailed || stepFailure === undefined) return
const assistantMessageID = yield* startAssistant() const assistantMessageID = yield* startAssistant()
stepFailed = true stepFailed = true
@@ -224,6 +227,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
sessionID: input.sessionID, sessionID: input.sessionID,
assistantMessageID, assistantMessageID,
error: stepFailure, error: stepFailure,
...usage,
}) })
}) })
@@ -409,12 +413,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
case "step-finish": case "step-finish":
yield* flush() yield* flush()
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish")) if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
if (event.reason === "content-filter") { if (event.reason === "content-filter") {
providerFailed = true providerFailed = true
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true) yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true)
return return
} }
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
return return
case "finish": case "finish":
return return
@@ -120,12 +120,12 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
const content = message.content.flatMap((item): ContentPart[] => { const content = message.content.flatMap((item): ContentPart[] => {
if (item.type === "text") return [{ type: "text", text: item.text }] if (item.type === "text") return [{ type: "text", text: item.text }]
if (item.type === "reasoning") if (item.type === "reasoning")
return sameModel return reuseProviderMetadata
? [ ? [
{ {
type: "reasoning", type: "reasoning",
text: item.text, text: item.text,
providerMetadata: reuseProviderMetadata ? providerMetadata(model.providerID, item.state) : undefined, providerMetadata: providerMetadata(model.providerID, item.state),
}, },
] ]
: item.text.length > 0 : item.text.length > 0
+1 -1
View File
@@ -41,7 +41,7 @@ Registrations are scoped:
## Permissions ## Permissions
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `apply_patch` declare the shared `edit` action. The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `patch` declare the shared `edit` action.
Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement. Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement.
+3 -3
View File
@@ -12,7 +12,7 @@ import { Patch } from "../patch"
import { PermissionV2 } from "../permission" import { PermissionV2 } from "../permission"
import { Tool } from "./tool" import { Tool } from "./tool"
export const name = "apply_patch" export const name = "patch"
export const Input = Schema.Struct({ export const Input = Schema.Struct({
patchText: Schema.String.annotate({ patchText: Schema.String.annotate({
@@ -91,11 +91,11 @@ export const Plugin = {
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" }) if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
const hunks = yield* Effect.try({ const hunks = yield* Effect.try({
try: () => Patch.parse(input.patchText), try: () => Patch.parse(input.patchText),
catch: (cause) => new ToolFailure({ message: `apply_patch verification failed: ${String(cause)}` }), catch: (cause) => new ToolFailure({ message: `patch verification failed: ${String(cause)}` }),
}) })
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" }) if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined) const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" }) if (move) return yield* new ToolFailure({ message: "patch moves are not supported yet" })
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = [] const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
for (const hunk of hunks) for (const hunk of hunks)
+1 -15
View File
@@ -43,20 +43,15 @@ export interface Registration {
readonly group?: string readonly group?: string
} }
export interface CodeModeTools {
[name: string]: Tool.Definition<never> | CodeModeTools
}
export const create = (options: { export const create = (options: {
readonly registrations: ReadonlyMap<string, Registration> readonly registrations: ReadonlyMap<string, Registration>
readonly current: (name: string) => Registration | undefined readonly current: (name: string) => Registration | undefined
readonly tools: CodeModeTools
}) => { }) => {
const runtime = ( const runtime = (
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>, invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks, hooks?: CodeMode.ToolCallHooks,
) => { ) => {
const tools = cloneTools(options.tools) const tools: Record<string, Tool.Definition<never> | Record<string, Tool.Definition<never>>> = {}
for (const [name, registration] of options.registrations) { for (const [name, registration] of options.registrations) {
const child = definition(name, registration.tool) const child = definition(name, registration.tool)
const value = Tool.make({ const value = Tool.make({
@@ -168,15 +163,6 @@ export const create = (options: {
}) })
} }
function cloneTools(tools: CodeModeTools): CodeModeTools {
return Object.assign(
Object.create(null),
Object.fromEntries(
Object.entries(tools).map(([name, value]) => [name, Tool.isDefinition(value) ? value : cloneTools(value)]),
),
)
}
function displayInput(input: unknown): Record<string, unknown> | undefined { function displayInput(input: unknown): Record<string, unknown> | undefined {
if (input === null || input === undefined) return if (input === null || input === undefined) return
if (typeof input !== "object" || Array.isArray(input)) return { input } if (typeof input !== "object" || Array.isArray(input)) return { input }
+6 -24
View File
@@ -1,5 +1,4 @@
export * as ToolRegistry from "./registry" export * as ToolRegistry from "./registry"
export type { CodeModeTools } from "./execute"
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm" import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
import { Context, Effect, Layer, Scope } from "effect" import { Context, Effect, Layer, Scope } from "effect"
@@ -10,12 +9,11 @@ import { SessionMessage } from "../session/message"
import { SessionSchema } from "../session/schema" import { SessionSchema } from "../session/schema"
import { ToolOutputStore } from "../tool-output-store" import { ToolOutputStore } from "../tool-output-store"
import { Wildcard } from "../util/wildcard" import { Wildcard } from "../util/wildcard"
import { ExecuteTool, type CodeModeTools } from "./execute" import { ExecuteTool } from "./execute"
import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool" import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool"
import { Tools } from "./tools" import { Tools } from "./tools"
import { ToolHooks } from "./hooks" import { ToolHooks } from "./hooks"
import { makeLocationNode } from "../effect/app-node" import { makeLocationNode } from "../effect/app-node"
import { LayerNode } from "../effect/layer-node"
import { SessionError } from "@opencode-ai/schema/session-error" import { SessionError } from "@opencode-ai/schema/session-error"
import { toSessionError } from "../session/to-session-error" import { toSessionError } from "../session/to-session-error"
@@ -53,20 +51,10 @@ export interface Settlement {
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools: CodeModeTools }>()(
"@opencode/v2/CodeModeCatalog",
) {}
const codeModeCatalogNode = makeLocationNode({
service: CodeModeCatalog,
layer: Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: {} })),
deps: [],
})
const registryLayer = Layer.effect( const registryLayer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const codeModeTools = (yield* CodeModeCatalog).tools
const resources = yield* ToolOutputStore.Service const resources = yield* ToolOutputStore.Service
const toolHooks = yield* ToolHooks.Service const toolHooks = yield* ToolHooks.Service
type Registration = { type Registration = {
@@ -203,10 +191,10 @@ const registryLayer = Layer.effect(
const registration = entries.at(-1)?.registration const registration = entries.at(-1)?.registration
if (registration) registrations.set(name, registration) if (registration) registrations.set(name, registration)
} }
// OpenAI/GPT models use apply_patch; every other model uses edit and write. // OpenAI/GPT models use patch; every other model uses edit and write.
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt") const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
for (const [name, registration] of registrations) { for (const [name, registration] of registrations) {
const wrongEditTool = name === "apply_patch" ? !usePatch : (name === "edit" || name === "write") && usePatch const wrongEditTool = name === "patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
if ( if (
wrongEditTool || wrongEditTool ||
(registration.deferred && !Flag.CODEMODE_ENABLED) || (registration.deferred && !Flag.CODEMODE_ENABLED) ||
@@ -216,13 +204,11 @@ const registryLayer = Layer.effect(
} }
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred)) const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred)) const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : {}
const execute = const execute =
(deferred.size > 0 || Object.keys(tools).length > 0) && !whollyDisabled("execute", input.permissions ?? []) deferred.size > 0 && !whollyDisabled("execute", input.permissions ?? [])
? ExecuteTool.create({ ? ExecuteTool.create({
registrations: deferred, registrations: deferred,
current: (name) => local.get(name)?.at(-1)?.registration, current: (name) => local.get(name)?.at(-1)?.registration,
tools,
}) })
: undefined : undefined
return { return {
@@ -255,18 +241,14 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
return rule?.resource === "*" && rule.effect === "deny" return rule?.resource === "*" && rule.effect === "deny"
} }
export function codeModeReplacement(tools: CodeModeTools): LayerNode.Replacement {
return [codeModeCatalogNode, Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools }))]
}
export const node = makeLocationNode({ export const node = makeLocationNode({
service: Service, service: Service,
layer, layer,
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode], deps: [ToolOutputStore.node, ToolHooks.node],
}) })
export const toolsNode = makeLocationNode({ export const toolsNode = makeLocationNode({
service: Tools.Service, service: Tools.Service,
layer, layer,
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode], deps: [ToolOutputStore.node, ToolHooks.node],
}) })
+2 -4
View File
@@ -51,13 +51,11 @@ it.effect("projects request settings, headers, and body overlays", () =>
apiKey: "secret", apiKey: "secret",
thinkingConfig: { thinkingBudget: 1024 }, thinkingConfig: { thinkingBudget: 1024 },
}) })
const resolved = yield* aisdk.model( const resolved = yield* aisdk.model({
{
...input, ...input,
headers: { "x-test": "header" }, headers: { "x-test": "header" },
body: { safety_setting: "strict" }, body: { safety_setting: "strict" },
}, })
)
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>( const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({ model: resolved, prompt: "Hello" }), LLM.request({ model: resolved, prompt: "Hello" }),
) )
+16 -1
View File
@@ -4,16 +4,27 @@ import {
CallToolRequestSchema, CallToolRequestSchema,
GetPromptRequestSchema, GetPromptRequestSchema,
ListPromptsRequestSchema, ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema, ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js" } from "@modelcontextprotocol/sdk/types.js"
const server = new Server({ name: "timeout", version: "1.0.0" }, { capabilities: { prompts: {}, tools: {} } }) const server = new Server(
{ name: "timeout", version: "1.0.0" },
{ capabilities: { prompts: {}, resources: {}, tools: {} } },
)
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
if (process.env.MCP_TIMEOUT_TARGET === "catalog") await Bun.sleep(100) if (process.env.MCP_TIMEOUT_TARGET === "catalog") await Bun.sleep(100)
return { tools: [{ name: "slow", inputSchema: { type: "object" } }] } return { tools: [{ name: "slow", inputSchema: { type: "object" } }] }
}) })
server.setRequestHandler(ListPromptsRequestSchema, () => Promise.resolve({ prompts: [{ name: "slow" }] })) server.setRequestHandler(ListPromptsRequestSchema, () => Promise.resolve({ prompts: [{ name: "slow" }] }))
server.setRequestHandler(ListResourcesRequestSchema, async () => {
if (process.env.MCP_TIMEOUT_TARGET === "resource-catalog") await Bun.sleep(100)
return { resources: [{ name: "slow", uri: "test://slow" }] }
})
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => Promise.resolve({ resourceTemplates: [] }))
server.setRequestHandler(CallToolRequestSchema, async () => { server.setRequestHandler(CallToolRequestSchema, async () => {
await Bun.sleep(100) await Bun.sleep(100)
return { content: [] } return { content: [] }
@@ -22,5 +33,9 @@ server.setRequestHandler(GetPromptRequestSchema, async () => {
await Bun.sleep(100) await Bun.sleep(100)
return { messages: [] } return { messages: [] }
}) })
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
await Bun.sleep(100)
return { contents: [{ uri: request.params.uri, text: "slow" }] }
})
await server.connect(new StdioServerTransport()) await server.connect(new StdioServerTransport())
+2 -5
View File
@@ -14,15 +14,12 @@ export const emptyMcpLayer = Layer.succeed(
instructions: () => Effect.succeed([]), instructions: () => Effect.succeed([]),
prompts: () => Effect.succeed([]), prompts: () => Effect.succeed([]),
prompt: () => Effect.succeed(undefined), prompt: () => Effect.succeed(undefined),
resourceCatalog: () => Effect.succeed(new MCP.ResourceCatalog({ resources: [], templates: [] })), resourceCatalog: () => Effect.succeed(MCP.ResourceCatalog.make({ resources: [], templates: [] })),
readResource: () => Effect.succeed(undefined), readResource: () => Effect.succeed(undefined),
}), }),
) )
export const emptyConfigLayer = Layer.succeed( export const emptyConfigLayer = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
Config.Service,
Config.Service.of({ entries: () => Effect.succeed([]) }),
)
export const testLocationLayer = Layer.succeed( export const testLocationLayer = Layer.succeed(
Location.Service, Location.Service,
+67 -1
View File
@@ -3,7 +3,7 @@ import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Config } from "@opencode-ai/schema/config" import { Config } from "@opencode-ai/schema/config"
import { Plugin } from "@opencode-ai/schema/plugin" import { Plugin } from "@opencode-ai/schema/plugin"
import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect" import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent" import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -12,6 +12,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LocationServiceMap } from "@opencode-ai/core/location-services" import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectV2 } from "@opencode-ai/core/project"
@@ -28,8 +29,43 @@ import { Reference } from "../src/reference"
import { ToolRegistry } from "../src/tool/registry" import { ToolRegistry } from "../src/tool/registry"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node]))) const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
const itWithSdk = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
)
describe("LocationServiceMap", () => { describe("LocationServiceMap", () => {
itWithSdk.live("preserves embedded SDK plugins after Location eviction", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const sdk = yield* SdkPlugins.Service
const locations = yield* LocationServiceMap.Service
const id = AgentV2.ID.make("persistent-sdk-agent")
const plugin = define({
id: "persistent-sdk-plugin",
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
})
yield* sdk.register(plugin)
const ref = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
const read = Effect.gen(function* () {
const supervisor = yield* PluginSupervisor.Service
yield* supervisor.ready
const agents = yield* AgentV2.Service
return yield* agents.get(id)
})
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
yield* locations.invalidate(ref)
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
}),
),
),
)
it.live("applies ordered plugin config operations during boot", () => it.live("applies ordered plugin config operations during boot", () =>
Effect.acquireRelease( Effect.acquireRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -172,6 +208,36 @@ describe("LocationServiceMap", () => {
), ),
) )
it.live("normalizes ref key shapes to one cached location graph", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.scoped(
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
const directory = AbsolutePath.make(dir.path)
const absent = Location.Ref.make({ directory })
const present = Location.Ref.make({ directory, workspaceID: undefined })
// The two shapes are not structurally Equal: own-key sets differ.
expect(Object.keys(absent)).toEqual(["directory"])
expect(Object.keys(present)).toEqual(["directory", "workspaceID"])
expect(Equal.equals(absent, present)).toBe(false)
const first = yield* locations.contextEffect(absent)
expect(yield* locations.contextEffect(present)).toBe(first)
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(1)
// Invalidating with the shape opposite to the one that booted must evict.
yield* locations.invalidate(present)
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(0)
}),
),
),
),
)
it.live("isolates catalog state by location", () => it.live("isolates catalog state by location", () =>
Effect.acquireRelease( Effect.acquireRelease(
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
+298 -2
View File
@@ -3,26 +3,165 @@ import { describe, expect, test } from "bun:test"
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js" import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
import { Server } from "@modelcontextprotocol/sdk/server/index.js" import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js" import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js"
import { ConfigMCP } from "@opencode-ai/core/config/mcp" import { ConfigMCP } from "@opencode-ai/core/config/mcp"
import { Config } from "@opencode-ai/core/config"
import { Credential } from "@opencode-ai/core/credential"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Form } from "@opencode-ai/core/form"
import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { MCP } from "@opencode-ai/core/mcp/index" import { MCP } from "@opencode-ai/core/mcp/index"
import { MCPClient } from "@opencode-ai/core/mcp/client" import { MCPClient } from "@opencode-ai/core/mcp/client"
import { PermissionV2 } from "@opencode-ai/core/permission" import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session" import { SessionV2 } from "@opencode-ai/core/session"
import { McpTool } from "@opencode-ai/core/tool/mcp" import { McpTool } from "@opencode-ai/core/tool/mcp"
import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { Deferred, Effect, Fiber, Layer, Stream } from "effect" import { Deferred, Effect, Exit, Fiber, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { location } from "./fixture/location"
import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool" import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
let calls = 0 let calls = 0
type ResourcePage = {
items: Array<{ name: string; uri: string; description?: string; mimeType?: string }>
nextCursor?: string
}
type ResourceTemplatePage = {
items: Array<{ name: string; uriTemplate: string; description?: string; mimeType?: string }>
nextCursor?: string
}
function resourceServer(input: { resources?: boolean; listChanged?: boolean } = {}) {
return Effect.acquireRelease(
Effect.promise(async () => {
const state = {
resources: [] as ResourcePage["items"],
templates: [] as ResourceTemplatePage["items"],
resourcePages: undefined as Record<string, ResourcePage> | undefined,
templatePages: undefined as Record<string, ResourceTemplatePage> | undefined,
contents: [
{ uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
] as Array<{ uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string }>,
resourceLists: 0,
templateLists: 0,
}
const protocol = new Server(
{ name: "mcp-resources", version: "1.0.0" },
{
capabilities: {
tools: {},
...(input.resources === false ? {} : { resources: { listChanged: input.listChanged } }),
},
},
)
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
if (input.resources !== false) {
protocol.setRequestHandler(ListResourcesRequestSchema, (request) => {
state.resourceLists += 1
const page = state.resourcePages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor })
})
protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => {
state.templateLists += 1
const page = state.templatePages?.[request.params?.cursor ?? "initial"]
return Promise.resolve({ resourceTemplates: page?.items ?? state.templates, nextCursor: page?.nextCursor })
})
protocol.setRequestHandler(ReadResourceRequestSchema, () => Promise.resolve({ contents: state.contents }))
}
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true,
})
await protocol.connect(transport)
const http = Bun.serve({
port: 0,
fetch: (request) => transport.handleRequest(request),
})
return {
state,
url: http.url.toString(),
sendResourceListChanged: () => protocol.sendResourceListChanged(),
close: async () => {
await protocol.close().catch(() => {})
await http.stop(true)
},
}
}),
(server) => Effect.promise(server.close),
)
}
function resourceMcpLayer(url: string) {
const directory = AbsolutePath.make(import.meta.dir)
const unusedIntegration = () => Effect.die("unused integration service")
return MCP.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
info: new Config.Info({
mcp: new ConfigMCP.Info({
servers: { resources: new ConfigMCP.Remote({ type: "remote", url, oauth: false }) },
}),
}),
}),
]),
}),
),
Layer.succeed(Location.Service, Location.Service.of(location({ directory }))),
Layer.mock(EventV2.Service, {
subscribe: () => Stream.never,
publish: (definition, data) =>
Effect.succeed({
id: EventV2.ID.create(),
type: definition.type,
data,
} as EventV2.Payload<typeof definition>),
}),
Layer.mock(Form.Service, {}),
Layer.mock(Integration.Service, {
connection: {
active: unusedIntegration,
resolve: unusedIntegration,
key: unusedIntegration,
oauth: unusedIntegration,
update: unusedIntegration,
remove: unusedIntegration,
},
attempt: {
status: unusedIntegration,
complete: unusedIntegration,
cancel: unusedIntegration,
},
}),
Layer.mock(Credential.Service, {}),
),
),
)
}
const mcp = Layer.mock(MCP.Service, { const mcp = Layer.mock(MCP.Service, {
tools: () => tools: () =>
Effect.succeed([ Effect.succeed([
@@ -241,6 +380,163 @@ test("applies the configured MCP execution timeout to prompts", async () => {
await expect(result).rejects.toThrow("Request timed out") await expect(result).rejects.toThrow("Request timed out")
}) })
test("applies configured MCP timeouts to resource operations", async () => {
const catalog = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const connection = yield* MCPClient.connect(
"resource-catalog-timeout",
new ConfigMCP.Local({
type: "local",
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-timeout.ts")],
environment: { MCP_TIMEOUT_TARGET: "resource-catalog" },
timeout: new ConfigMCP.Timeout({ catalog: 10 }),
}),
import.meta.dir,
)
return yield* connection.resources()
}),
),
)
await expect(catalog).rejects.toThrow("Request timed out")
const read = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const connection = yield* MCPClient.connect(
"resource-read-timeout",
new ConfigMCP.Local({
type: "local",
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-timeout.ts")],
timeout: new ConfigMCP.Timeout({ execution: 10 }),
}),
import.meta.dir,
)
return yield* connection.readResource({ uri: "test://slow" })
}),
),
)
await expect(read).rejects.toThrow("Request timed out")
})
test("lists, reads, and reports MCP resource changes", async () => {
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const server = yield* resourceServer({ listChanged: true })
server.state.resourcePages = {
initial: {
items: [{ name: "Readme", uri: "docs://readme", description: "Project docs" }],
nextCursor: "resources-2",
},
"resources-2": { items: [{ name: "Logo", uri: "docs://logo", mimeType: "image/png" }] },
}
server.state.templatePages = {
initial: {
items: [{ name: "File", uriTemplate: "docs://{path}" }],
nextCursor: "templates-2",
},
"templates-2": { items: [{ name: "Issue", uriTemplate: "issue://{id}", description: "Issue" }] },
}
const connection = yield* MCPClient.connect(
"resources",
new ConfigMCP.Remote({ type: "remote", url: server.url, oauth: false }),
import.meta.dir,
)
expect(yield* connection.resources()).toEqual([
{ name: "Readme", uri: "docs://readme", description: "Project docs", mimeType: undefined },
{ name: "Logo", uri: "docs://logo", description: undefined, mimeType: "image/png" },
])
expect(yield* connection.resourceTemplates()).toEqual([
{ name: "File", uriTemplate: "docs://{path}", description: undefined, mimeType: undefined },
{ name: "Issue", uriTemplate: "issue://{id}", description: "Issue", mimeType: undefined },
])
expect(yield* connection.readResource({ uri: "docs://readme" })).toEqual({
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
})
const changed = yield* Deferred.make<void>()
connection.onResourcesChanged(() => Deferred.doneUnsafe(changed, Exit.void))
yield* Effect.promise(server.sendResourceListChanged)
yield* Deferred.await(changed)
}),
),
)
})
test("skips MCP resource requests when the capability is absent", async () => {
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const server = yield* resourceServer({ resources: false })
const connection = yield* MCPClient.connect(
"resources",
new ConfigMCP.Remote({ type: "remote", url: server.url, oauth: false }),
import.meta.dir,
)
expect(yield* connection.resources()).toEqual([])
expect(yield* connection.resourceTemplates()).toEqual([])
expect(yield* connection.readResource({ uri: "docs://readme" })).toBeUndefined()
expect({ resources: server.state.resourceLists, templates: server.state.templateLists }).toEqual({
resources: 0,
templates: 0,
})
}),
),
)
})
test("loads and reads MCP resources", async () => {
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const server = yield* resourceServer()
server.state.resources = [{ name: "Readme", uri: "docs://readme" }]
server.state.templates = [{ name: "File", uriTemplate: "docs://{path}" }]
yield* Effect.gen(function* () {
const service = yield* MCP.Service
expect(yield* service.resourceCatalog()).toEqual({
resources: [
{
server: "resources",
name: "Readme",
uri: "docs://readme",
description: undefined,
mimeType: undefined,
},
],
templates: [
{
server: "resources",
name: "File",
uriTemplate: "docs://{path}",
description: undefined,
mimeType: undefined,
},
],
})
server.state.resources = [{ name: "Guide", uri: "docs://guide" }]
expect((yield* service.resourceCatalog()).resources.map((resource) => resource.uri)).toEqual(["docs://guide"])
expect(yield* service.readResource({ server: "resources", uri: "docs://readme" })).toEqual({
server: "resources",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
})
}).pipe(Effect.provide(resourceMcpLayer(server.url)))
}),
),
)
})
it.effect("advertises MCP output schemas to Code Mode", () => it.effect("advertises MCP output schemas to Code Mode", () =>
Effect.gen(function* () { Effect.gen(function* () {
const registry = yield* ToolRegistry.Service const registry = yield* ToolRegistry.Service
+13 -3
View File
@@ -52,6 +52,15 @@ const it = testEffect(
), ),
) )
test("compaction prompt preserves detailed work state and relevant files", () => {
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
expect(prompt).toContain("## Work State\n### Completed")
expect(prompt).toContain("### Active")
expect(prompt).toContain("### Blocked")
expect(prompt).toContain("## Relevant Files")
})
test("compaction describes tool media without embedding base64", () => { test("compaction describes tool media without embedding base64", () => {
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
const serialized = SessionCompaction.serializeToolContent([ const serialized = SessionCompaction.serializeToolContent([
@@ -74,13 +83,14 @@ test("compaction prompt requires the checkpoint headings in order", () => {
"## Objective", "## Objective",
"## Important Details", "## Important Details",
"## Work State", "## Work State",
"### Completed",
"### Active",
"### Blocked",
"## Next Move", "## Next Move",
"## Relevant Files",
]) ])
expect(prompt).toContain("one or two brief sentences") expect(prompt).toContain("one or two brief sentences")
expect(prompt).toContain("constraints/preferences, decisions and why") expect(prompt).toContain("constraints/preferences, decisions and why")
expect(prompt).toContain("Completed:")
expect(prompt).toContain("Active:")
expect(prompt).toContain("Blocked:")
expect(prompt).toContain("immediate concrete action") expect(prompt).toContain("immediate concrete action")
expect(prompt).toContain("next action if known") expect(prompt).toContain("next action if known")
expect(prompt).toContain("Keep every section, even when empty.") expect(prompt).toContain("Keep every section, even when empty.")
+39 -3
View File
@@ -225,9 +225,17 @@ describe("SessionV2.create", () => {
promotedSeq: 2, promotedSeq: 2,
}) })
yield* session.prompt({ sessionID: parent.id, prompt: PromptInput.Prompt.make({ text: "Parent changed" }), resume: false }) yield* session.prompt({
sessionID: parent.id,
prompt: PromptInput.Prompt.make({ text: "Parent changed" }),
resume: false,
})
yield* SessionInput.promoteSteers(db, events, parent.id) yield* SessionInput.promoteSteers(db, events, parent.id)
yield* session.prompt({ sessionID: forked.id, prompt: PromptInput.Prompt.make({ text: "Child continues" }), resume: false }) yield* session.prompt({
sessionID: forked.id,
prompt: PromptInput.Prompt.make({ text: "Child continues" }),
resume: false,
})
yield* SessionInput.promoteSteers(db, events, forked.id) yield* SessionInput.promoteSteers(db, events, forked.id)
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
@@ -260,8 +268,25 @@ describe("SessionV2.create", () => {
resume: false, resume: false,
}) })
yield* SessionInput.promoteSteers(db, events, parent.id) yield* SessionInput.promoteSteers(db, events, parent.id)
const assistantMessageID = SessionMessage.ID.create()
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
yield* events.publish(SessionEvent.Step.Started, {
sessionID: parent.id,
assistantMessageID,
agent: "build",
model,
})
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: parent.id,
assistantMessageID,
finish: "stop",
cost: 0.75,
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
})
const forked = yield* session.fork({ sessionID: parent.id, messageID: second.id }) const forked = yield* session.fork({ sessionID: parent.id, messageID: second.id })
const beforeFirst = yield* session.fork({ sessionID: parent.id, messageID: first.id })
const complete = yield* session.fork({ sessionID: parent.id })
const context = yield* session.context(forked.id) const context = yield* session.context(forked.id)
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id))) const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
@@ -269,6 +294,13 @@ describe("SessionV2.create", () => {
expect(context).toMatchObject([{ text: "First" }]) expect(context).toMatchObject([{ text: "First" }])
expect(context[0]?.id).not.toBe(first.id) expect(context[0]?.id).not.toBe(first.id)
expect(history[0]).toMatchObject({ data: { from: second.id } }) expect(history[0]).toMatchObject({ data: { from: second.id } })
expect(forked).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
expect(yield* session.context(beforeFirst.id)).toEqual([])
expect(beforeFirst).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
expect(complete).toMatchObject({
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
}), }),
) )
@@ -375,7 +407,11 @@ describe("SessionV2.create", () => {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const created = yield* session.create({ location }) const created = yield* session.create({ location })
yield* session.prompt({ sessionID: created.id, prompt: PromptInput.Prompt.make({ text: "Hello" }), resume: false }) yield* session.prompt({
sessionID: created.id,
prompt: PromptInput.Prompt.make({ text: "Hello" }),
resume: false,
})
yield* SessionInput.promoteSteers(db, events, created.id) yield* SessionInput.promoteSteers(db, events, created.id)
expect( expect(
+59 -6
View File
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { DateTime, Effect, Schema } from "effect" import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect"
import { asc, eq } from "drizzle-orm" import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -41,12 +41,15 @@ const assistantRow = (
id: SessionMessage.ID, id: SessionMessage.ID,
seq: number, seq: number,
time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created }, time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created },
usage?: Pick<SessionMessage.Assistant, "cost" | "tokens">,
) => { ) => {
const { const {
id: _, id: _,
type, type,
...data ...data
} = encodeMessage(SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time })) } = encodeMessage(
SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time, ...usage }),
)
return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data } return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
} }
@@ -67,6 +70,12 @@ describe("SessionProjector", () => {
directory: "/project", directory: "/project",
title: "test", title: "test",
version: "test", version: "test",
cost: 1.25,
tokens_input: 10,
tokens_output: 4,
tokens_reasoning: 2,
tokens_cache_read: 3,
tokens_cache_write: 1,
}) })
.run() .run()
const boundary = SessionMessage.ID.make("msg_boundary") const boundary = SessionMessage.ID.make("msg_boundary")
@@ -75,8 +84,24 @@ describe("SessionProjector", () => {
.insert(SessionMessageTable) .insert(SessionMessageTable)
.values([ .values([
assistantRow(earlier, 0), assistantRow(earlier, 0),
assistantRow(boundary, 1), assistantRow(
assistantRow(SessionMessage.ID.make("msg_later"), 2), boundary,
1,
{ created },
{
cost: 0.5,
tokens: { input: 4, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
},
),
assistantRow(
SessionMessage.ID.make("msg_later"),
2,
{ created },
{
cost: 0.75,
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
},
),
]) ])
.run() .run()
yield* db yield* db
@@ -106,6 +131,14 @@ describe("SessionProjector", () => {
expect( expect(
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id), (yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
).toEqual([earlier]) ).toEqual([earlier])
expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()).toMatchObject({
cost: 1.25,
tokens_input: 10,
tokens_output: 4,
tokens_reasoning: 2,
tokens_cache_read: 3,
tokens_cache_write: 1,
})
// A committed revert resets the context checkpoint so the next turn re-initializes. // A committed revert resets the context checkpoint so the next turn re-initializes.
expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined() expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
}), }),
@@ -534,12 +567,15 @@ describe("SessionProjector", () => {
.pipe(Effect.orDie) .pipe(Effect.orDie)
const service = yield* EventV2.Service const service = yield* EventV2.Service
const usageUpdated = yield* service
.subscribe(SessionEvent.UsageUpdated)
.pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true }))
yield* service.publish(SessionEvent.Step.Ended, { yield* service.publish(SessionEvent.Step.Ended, {
sessionID, sessionID,
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"), assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
finish: "stop", finish: "stop",
cost: 0, cost: 1.25,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
}) })
const rows = yield* db const rows = yield* db
@@ -556,8 +592,25 @@ describe("SessionProjector", () => {
expect(messages[1]).toMatchObject({ expect(messages[1]).toMatchObject({
type: "assistant", type: "assistant",
finish: "stop", finish: "stop",
cost: 1.25,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
time: { completed: DateTime.makeUnsafe(0) }, time: { completed: DateTime.makeUnsafe(0) },
}) })
expect(
yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
).toMatchObject({
cost: 1.25,
tokens_input: 10,
tokens_output: 4,
tokens_reasoning: 2,
tokens_cache_read: 3,
tokens_cache_write: 1,
})
expect(Option.getOrThrow(yield* Fiber.join(usageUpdated)).data).toEqual({
sessionID,
cost: 1.25,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
})
}), }),
) )
@@ -461,7 +461,7 @@ Recent work
]) ])
}) })
test("drops provider-native continuation metadata from failed assistant turns", () => { test("lowers failed assistant reasoning to text", () => {
const messages = toLLMMessages( const messages = toLLMMessages(
[ [
SessionMessage.Assistant.make({ SessionMessage.Assistant.make({
@@ -501,7 +501,7 @@ Recent work
) )
expect(messages[0]?.content).toEqual([ expect(messages[0]?.content).toEqual([
{ type: "reasoning", text: "Partial thought", providerMetadata: undefined }, { type: "text", text: "Partial thought" },
{ {
type: "tool-call", type: "tool-call",
id: "hosted-failed", id: "hosted-failed",
@@ -151,15 +151,39 @@ test("step finish records settlement without publishing step ended", async () =>
test("content-filter finish retains failure evidence until step closeout", async () => { test("content-filter finish retains failure evidence until step closeout", async () => {
const { published, publisher } = capture() const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 }))) await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "content-filter" }))) await Effect.runPromise(
publisher.publish(
LLMEvent.stepFinish({
index: 0,
reason: "content-filter",
usage: {
nonCachedInputTokens: 8,
outputTokens: 3,
reasoningTokens: 1,
},
}),
),
)
expect(published.map((event) => event.type)).toEqual(["session.step.started.1"]) expect(published.map((event) => event.type)).toEqual(["session.step.started.1"])
await Effect.runPromise(publisher.publishStepFailure()) const settlement = publisher.stepSettlement()
expect(settlement).toMatchObject({
finish: "content-filter",
tokens: { input: 8, output: 2, reasoning: 1 },
})
if (!settlement) throw new Error("Expected content-filter settlement")
await Effect.runPromise(
publisher.publishStepFailure({
cost: 1.25,
tokens: settlement.tokens,
}),
)
expect(published.map((event) => event.type)).toEqual(["session.step.started.1", "session.step.failed.1"]) expect(published.map((event) => event.type)).toEqual(["session.step.started.1", "session.step.failed.1"])
expect(published.at(-1)?.data).toMatchObject({ expect(published.at(-1)?.data).toMatchObject({
error: { type: "provider.content-filter", message: "Provider blocked the response" }, error: { type: "provider.content-filter", message: "Provider blocked the response" },
cost: 1.25,
tokens: { input: 8, output: 2, reasoning: 1 },
}) })
expect(publisher.stepSettlement()).toBeUndefined()
}) })
test("content-filter finish preserves partial streamed text and never ends the step successfully", async () => { test("content-filter finish preserves partial streamed text and never ends the step successfully", async () => {
@@ -30,27 +30,6 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
}) })
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]]) const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
const it = testEffect(registryLayer) const it = testEffect(registryLayer)
const codeModeTools: ToolRegistry.CodeModeTools = {
opencode: {
v2: {
health: {
get: {
_tag: "CodeModeTool" as const,
description: "Get server health",
input: Schema.Struct({}),
output: Schema.Struct({ healthy: Schema.Boolean }),
run: () => Effect.succeed({ healthy: true }),
},
},
},
},
}
const codeModeIt = testEffect(
AppNodeBuilder.build(ToolRegistry.node, [
[ToolOutputStore.node, outputStore],
ToolRegistry.codeModeReplacement(codeModeTools),
]),
)
const identity = { const identity = {
agent: AgentV2.ID.make("build"), agent: AgentV2.ID.make("build"),
assistantMessageID: SessionMessage.ID.make("msg_registry"), assistantMessageID: SessionMessage.ID.make("msg_registry"),
@@ -74,50 +53,6 @@ const make = (permission?: string) => {
} }
describe("ToolRegistry", () => { describe("ToolRegistry", () => {
codeModeIt.effect("includes host Code Mode trees without hosted tool registration", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
const definitions = yield* toolDefinitions(service)
expect(definitions.map((tool) => tool.name)).toEqual(["execute"])
expect(definitions[0]?.description).toContain("tools.opencode.v2.health.get")
expect(
yield* executeTool(service, {
sessionID,
...identity,
call: {
type: "tool-call",
id: "call-opencode-health",
name: "execute",
input: { code: "return await tools.opencode.v2.health.get({})" },
},
}),
).toEqual({ type: "text", value: '{\n "healthy": true\n}' })
}),
)
codeModeIt.effect("keeps host Code Mode trees immutable while merging deferred tools", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
yield* service.register({ echo: make() }, { group: "opencode", deferred: true })
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
expect(
yield* executeTool(service, {
sessionID,
...identity,
call: {
type: "tool-call",
id: "call-opencode-echo",
name: "execute",
input: { code: 'return await tools.opencode.echo({ text: "hello" })' },
},
}),
).toEqual({ type: "text", value: '{\n "text": "hello"\n}' })
}),
)
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
Effect.gen(function* () { Effect.gen(function* () {
const service = yield* ToolRegistry.Service const service = yield* ToolRegistry.Service
@@ -154,16 +89,16 @@ describe("ToolRegistry", () => {
read: make(), read: make(),
edit: make("edit"), edit: make("edit"),
write: make("edit"), write: make("edit"),
apply_patch: make("edit"), patch: make("edit"),
}) })
const names = (model: ToolRegistry.MaterializeInput["model"]) => const names = (model: ToolRegistry.MaterializeInput["model"]) =>
service service
.materialize({ model }) .materialize({ model })
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name))) .pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "apply_patch"]) expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "patch"])
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "apply_patch"]) expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "apply_patch"]) expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "patch"])
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"]) expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
}), }),
) )
+35 -2
View File
@@ -1,4 +1,4 @@
import { describe, expect } from "bun:test" import { describe, expect, test } from "bun:test"
import { import {
LLMClient, LLMClient,
LLMError, LLMError,
@@ -116,6 +116,28 @@ const recoveryModel = Model.make({
provider: "fake", provider: "fake",
route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }), route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
}) })
test("calculates step cost using the matching context tier", () => {
expect(
SessionRunnerLLM.calculateCost(
[
{ input: 1, output: 2, cache: { read: 0.1, write: 0.5 } },
{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } },
],
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 1 } },
),
).toBeCloseTo(0.0002926)
})
test("does not apply an ineligible tier without base pricing", () => {
expect(
SessionRunnerLLM.calculateCost(
[{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } }],
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 0 } },
),
).toBe(0)
})
const authorizations: Tool.Context[] = [] const authorizations: Tool.Context[] = []
const executions: string[] = [] const executions: string[] = []
const permission = Layer.succeed( const permission = Layer.succeed(
@@ -1704,6 +1726,7 @@ describe("SessionRunnerLLM", () => {
{ {
type: "assistant", type: "assistant",
finish: "tool-calls", finish: "tool-calls",
cost: 0,
tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } }, tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } },
content: [ content: [
{ type: "reasoning", text: "Think" }, { type: "reasoning", text: "Think" },
@@ -3635,7 +3658,11 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepStart({ index: 0 }), LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "partial" }), LLMEvent.textStart({ id: "partial" }),
LLMEvent.textDelta({ id: "partial", text: "Partial" }), LLMEvent.textDelta({ id: "partial", text: "Partial" }),
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }), LLMEvent.stepFinish({
index: 0,
reason: "content-filter",
usage: { nonCachedInputTokens: 8, outputTokens: 3, reasoningTokens: 1 },
}),
LLMEvent.finish({ reason: "content-filter" }), LLMEvent.finish({ reason: "content-filter" }),
] ]
@@ -3646,9 +3673,15 @@ describe("SessionRunnerLLM", () => {
type: "assistant", type: "assistant",
finish: "error", finish: "error",
error: { type: "provider.content-filter" }, error: { type: "provider.content-filter" },
cost: 0,
tokens: { input: 8, output: 2, reasoning: 1, cache: { read: 0, write: 0 } },
content: [{ type: "text", text: "Partial" }], content: [{ type: "text", text: "Partial" }],
}, },
]) ])
expect(yield* session.get(sessionID)).toMatchObject({
cost: 0,
tokens: { input: 8, output: 2, reasoning: 1, cache: { read: 0, write: 0 } },
})
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1") expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
}), }),
) )
+5 -5
View File
@@ -26,7 +26,7 @@ const applyPatchToolNode = makeLocationNode({
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
}) })
const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test") const sessionID = SessionV2.ID.make("ses_patch_tool_test")
const assertions: PermissionV2.AssertInput[] = [] const assertions: PermissionV2.AssertInput[] = []
let denyAction: string | undefined let denyAction: string | undefined
let failRemoveTarget: string | undefined let failRemoveTarget: string | undefined
@@ -132,10 +132,10 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
const call = (patchText: string, id = "call-apply-patch") => ({ const call = (patchText: string, id = "call-apply-patch") => ({
sessionID, sessionID,
...toolIdentity, ...toolIdentity,
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } }, call: { type: "tool-call" as const, id, name: "patch", input: { patchText } },
}) })
// apply_patch is only materialized for OpenAI/GPT models. // patch is only materialized for OpenAI/GPT models.
const model = { id: "gpt-5", provider: "openai" } const model = { id: "gpt-5", provider: "openai" }
const exists = (target: string) => const exists = (target: string) =>
@@ -162,7 +162,7 @@ describe("ApplyPatchTool", () => {
withTool(tmp.path, (registry) => withTool(tmp.path, (registry) =>
Effect.gen(function* () { Effect.gen(function* () {
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([ expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
"apply_patch", "patch",
]) ])
const settled = yield* settleTool( const settled = yield* settleTool(
registry, registry,
@@ -241,7 +241,7 @@ describe("ApplyPatchTool", () => {
), ),
model, model,
), ),
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" }) ).toEqual({ type: "error", value: "patch moves are not supported yet" })
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false) expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
expect(assertions).toEqual([]) expect(assertions).toEqual([])
}), }),
+5 -13
View File
@@ -17,7 +17,12 @@
"tabs": [ "tabs": [
{ {
"tab": "Docs", "tab": "Docs",
"groups": [
{
"group": "Get started",
"pages": ["index", "config", "plugins", "troubleshooting"] "pages": ["index", "config", "plugins", "troubleshooting"]
}
]
}, },
{ {
"tab": "SDK", "tab": "SDK",
@@ -39,19 +44,6 @@
], ],
"global": {} "global": {}
}, },
"navbar": {
"links": [
{
"label": "Discord",
"href": "https://opencode.ai/discord"
}
],
"primary": {
"type": "button",
"label": "GitHub",
"href": "https://github.com/anomalyco/opencode"
}
},
"contextual": { "contextual": {
"options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"] "options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"]
}, },
+1
View File
@@ -6,6 +6,7 @@ const patterns = [
/input is too long for requested model/i, /input is too long for requested model/i,
/exceeds the context window/i, /exceeds the context window/i,
/input token count.*exceeds the maximum/i, /input token count.*exceeds the maximum/i,
/tokens in request more than max tokens allowed/i,
/maximum prompt length is \d+/i, /maximum prompt length is \d+/i,
/reduce the length of the messages/i, /reduce the length of the messages/i,
/maximum context length is \d+ tokens/i, /maximum context length is \d+ tokens/i,
+8
View File
@@ -0,0 +1,8 @@
import { describe, expect, test } from "bun:test"
import { isContextOverflow } from "../src"
describe("provider error classification", () => {
test("classifies Z.AI GLM token limit messages as context overflow", () => {
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
})
})
@@ -40,7 +40,6 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"), enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"), enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
experimentalReferences: enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES"), experimentalReferences: enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES"),
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"), experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"),
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"), experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"), experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"),
@@ -3,7 +3,6 @@ import { Agent } from "@/agent/agent"
import { Job } from "@/job" import { Job } from "@/job"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { MCP } from "@/mcp" import { MCP } from "@/mcp"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Session } from "@/session/session" import { Session } from "@/session/session"
@@ -34,10 +33,9 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
const worktreeSvc = yield* Worktree.Service const worktreeSvc = yield* Worktree.Service
const sessions = yield* Session.Service const sessions = yield* Session.Service
const jobs = yield* Job.Service const jobs = yield* Job.Service
const flags = yield* RuntimeFlags.Service
const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () { const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () {
return { backgroundSubagents: flags.experimentalBackgroundSubagents } return { backgroundSubagents: true }
}) })
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
@@ -159,7 +157,6 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
const sessionBackground = Effect.fn("ExperimentalHttpApi.sessionBackground")(function* (ctx: { const sessionBackground = Effect.fn("ExperimentalHttpApi.sessionBackground")(function* (ctx: {
params: { sessionID: SessionID } params: { sessionID: SessionID }
}) { }) {
if (!flags.experimentalBackgroundSubagents) return false
return (yield* jobs.backgroundAll({ sessionID: ctx.params.sessionID, type: "task" })).length > 0 return (yield* jobs.backgroundAll({ sessionID: ctx.params.sessionID, type: "task" })).length > 0
}) })
+1 -14
View File
@@ -1,6 +1,5 @@
import * as Tool from "./tool" import * as Tool from "./tool"
import DESCRIPTION from "./task.txt" import DESCRIPTION from "./task.txt"
import { ToolJsonSchema } from "./json-schema"
import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Job } from "@/job" import { Job } from "@/job"
import { Session } from "@/session/session" import { Session } from "@/session/session"
@@ -12,7 +11,6 @@ import type { SessionPrompt } from "../session/prompt"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { Effect, Exit, Schema, Scope } from "effect" import { Effect, Exit, Schema, Scope } from "effect"
import { EffectBridge } from "@/effect/bridge" import { EffectBridge } from "@/effect/bridge"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
export interface TaskPromptOps { export interface TaskPromptOps {
@@ -51,8 +49,6 @@ const BaseParameterFields = {
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }), command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
} }
const BaseParameters = Schema.Struct(BaseParameterFields)
export const Parameters = Schema.Struct({ export const Parameters = Schema.Struct({
...BaseParameterFields, ...BaseParameterFields,
background: Schema.optional(Schema.Boolean).annotate({ background: Schema.optional(Schema.Boolean).annotate({
@@ -86,7 +82,6 @@ export const TaskTool = Tool.define(
const config = yield* Config.Service const config = yield* Config.Service
const sessions = yield* Session.Service const sessions = yield* Session.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service const database = yield* Database.Service
const run = Effect.fn("TaskTool.execute")(function* ( const run = Effect.fn("TaskTool.execute")(function* (
@@ -95,11 +90,6 @@ export const TaskTool = Tool.define(
) { ) {
const cfg = yield* config.get() const cfg = yield* config.get()
const runInBackground = params.background === true const runInBackground = params.background === true
if (runInBackground && !flags.experimentalBackgroundSubagents) {
return yield* Effect.fail(
new Error("Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"),
)
}
if (!ctx.extra?.bypassAgentCheck) { if (!ctx.extra?.bypassAgentCheck) {
yield* ctx.ask({ yield* ctx.ask({
@@ -333,11 +323,8 @@ export const TaskTool = Tool.define(
}) })
return { return {
description: flags.experimentalBackgroundSubagents description: [DESCRIPTION, BACKGROUND_DESCRIPTION].join("\n\n"),
? [DESCRIPTION, BACKGROUND_DESCRIPTION].join("\n\n")
: DESCRIPTION,
parameters: Parameters, parameters: Parameters,
jsonSchema: flags.experimentalBackgroundSubagents ? undefined : ToolJsonSchema.fromSchema(BaseParameters),
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) => execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
run(params, ctx).pipe(Effect.orDie), run(params, ctx).pipe(Effect.orDie),
} }
@@ -161,7 +161,6 @@ async function renderFooter(
currentModel?: RunInput["model"] currentModel?: RunInput["model"]
currentVariant?: string currentVariant?: string
subagents?: FooterSubagentState subagents?: FooterSubagentState
backgroundSubagents?: boolean
width?: number width?: number
height?: number height?: number
state?: Partial<FooterState> state?: Partial<FooterState>
@@ -199,7 +198,6 @@ async function renderFooter(
subagent={subagents} subagent={subagents}
theme={input.theme ?? (() => RUN_THEME_FALLBACK)} theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
tuiConfig={config} tuiConfig={config}
backgroundSubagents={input.backgroundSubagents ?? true}
agent="opencode" agent="opencode"
onSubmit={input.onSubmit ?? (() => true)} onSubmit={input.onSubmit ?? (() => true)}
onPermissionReply={() => {}} onPermissionReply={() => {}}
@@ -998,7 +996,6 @@ test("direct footer shows editable prompts and additional queued work while runn
]} ]}
theme={() => RUN_THEME_FALLBACK} theme={() => RUN_THEME_FALLBACK}
tuiConfig={tuiConfig} tuiConfig={tuiConfig}
backgroundSubagents={true}
agent="opencode" agent="opencode"
onSubmit={() => true} onSubmit={() => true}
onPermissionReply={() => {}} onPermissionReply={() => {}}
@@ -1071,7 +1068,7 @@ test("direct footer shows editable prompts and additional queued work while runn
} }
}) })
test("direct footer separates a lone context hint from model and command hint", async () => { test("direct footer always offers backgrounding for a foreground subagent", async () => {
const app = await renderFooter({ const app = await renderFooter({
providers: [provider()], providers: [provider()],
currentModel: { providerID: "opencode", modelID: "gpt-5" }, currentModel: { providerID: "opencode", modelID: "gpt-5" },
@@ -1082,7 +1079,6 @@ test("direct footer separates a lone context hint from model and command hint",
permissions: [], permissions: [],
questions: [], questions: [],
}, },
backgroundSubagents: false,
width: 160, width: 160,
}) })
@@ -1091,8 +1087,8 @@ test("direct footer separates a lone context hint from model and command hint",
const frame = app.captureCharFrame() const frame = app.captureCharFrame()
expect(frame).toContain("GPT-5") expect(frame).toContain("GPT-5")
expect(frame).toContain("xhigh · ctrl+x down subagents · ctrl+p cmd") expect(frame).toContain("xhigh · ctrl+b background · ctrl+x down subagents · ctrl+p cmd")
expect(frame).not.toContain("ctrl+b background") expect(frame).toContain("ctrl+b background")
expect(frame).not.toContain("queued") expect(frame).not.toContain("queued")
} finally { } finally {
app.cleanup() app.cleanup()
@@ -1110,7 +1106,6 @@ test("direct footer hides the subagent hint when only completed subagents remain
permissions: [], permissions: [],
questions: [], questions: [],
}, },
backgroundSubagents: false,
width: 160, width: 160,
}) })
@@ -134,7 +134,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async () => { createRuntimeLifecycle: async () => {
@@ -233,7 +232,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async () => { createRuntimeLifecycle: async () => {
@@ -406,7 +404,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: true, thinking: true,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async (input) => { createRuntimeLifecycle: async (input) => {
@@ -496,7 +493,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async (input) => { createRuntimeLifecycle: async (input) => {
@@ -557,7 +553,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async () => { createRuntimeLifecycle: async () => {
@@ -603,7 +598,6 @@ describe("run interactive runtime", () => {
variant: undefined, variant: undefined,
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async (input) => { createRuntimeLifecycle: async (input) => {
@@ -716,7 +710,6 @@ describe("run interactive runtime", () => {
variant: "low", variant: "low",
files: [], files: [],
thinking: false, thinking: false,
backgroundSubagents: false,
}, },
{ {
createRuntimeLifecycle: async (input) => { createRuntimeLifecycle: async (input) => {
@@ -51,7 +51,6 @@ describe("RuntimeFlags", () => {
expect(flags.enableExperimentalModels).toBe(true) expect(flags.enableExperimentalModels).toBe(true)
expect(flags.enableQuestionTool).toBe(true) expect(flags.enableQuestionTool).toBe(true)
expect(flags.experimentalReferences).toBe(true) expect(flags.experimentalReferences).toBe(true)
expect(flags.experimentalBackgroundSubagents).toBe(true)
expect(flags.experimentalLspTy).toBe(false) expect(flags.experimentalLspTy).toBe(false)
expect(flags.experimentalLspTool).toBe(true) expect(flags.experimentalLspTool).toBe(true)
expect(flags.experimentalOxfmt).toBe(true) expect(flags.experimentalOxfmt).toBe(true)
@@ -579,8 +579,10 @@ const scenarios: Scenario[] = [
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() })) .at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
.json(200, array), .json(200, array),
http.protected.get("/experimental/capabilities", "experimental.capabilities.get").json(200, (body) => { http.protected.get("/experimental/capabilities", "experimental.capabilities.get").json(200, (body) => {
check(typeof body === "object" && body !== null, "capabilities should be an object") check(
check("backgroundSubagents" in body, "capabilities should report background subagents") typeof body === "object" && body !== null && "backgroundSubagents" in body && body.backgroundSubagents === true,
"capabilities should report background subagents as available",
)
}), }),
http.protected http.protected
.post("/experimental/session/{sessionID}/background", "experimental.session.background") .post("/experimental/session/{sessionID}/background", "experimental.session.background")
@@ -1,12 +1,13 @@
import { afterEach, describe, expect, mock } from "bun:test" import { afterEach, describe, expect, mock } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect" import { Effect, Fiber, Layer } from "effect"
import { Session as SessionNs } from "@/session/session" import { Session as SessionNs } from "@/session/session"
import { Job } from "@/job"
import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { pollWithTimeout, testEffect } from "../lib/effect"
import { httpApiLayer, requestInDirectory } from "./httpapi-layer" import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer)) const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), LayerNode.compile(Job.node), httpApiLayer))
afterEach(async () => { afterEach(async () => {
mock.restore() mock.restore()
@@ -14,6 +15,19 @@ afterEach(async () => {
}) })
describe("session action routes", () => { describe("session action routes", () => {
it.instance(
"reports background subagents as available",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const res = yield* requestInDirectory("/experimental/capabilities", test.directory)
expect(res.status).toBe(200)
expect(yield* res.json).toEqual({ backgroundSubagents: true })
}),
{ git: true },
)
it.instance( it.instance(
"session routes expose metadata on create, update, get, and fork", "session routes expose metadata on create, update, get, and fork",
() => () =>
@@ -107,4 +121,33 @@ describe("session action routes", () => {
}), }),
{ git: true }, { git: true },
) )
it.instance(
"experimental background route backgrounds a synchronous subagent",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const session = yield* Effect.acquireRelease(SessionNs.use.create({}), (created) =>
SessionNs.use.remove(created.id).pipe(Effect.ignore),
)
const jobs = yield* Job.Service
const job = yield* jobs.start({ type: "task", run: Effect.never })
const waiting = yield* jobs.block({ id: job.id, sessionID: session.id }).pipe(Effect.forkChild)
const backgrounded = yield* pollWithTimeout(
requestInDirectory(`/experimental/session/${session.id}/background`, test.directory, {
method: "POST",
}).pipe(
Effect.flatMap((res) => res.json),
Effect.map((value) => (value === true ? true : undefined)),
),
"background route never released the synchronous subagent",
)
expect(backgrounded).toBe(true)
expect(yield* Fiber.join(waiting)).toMatchObject({ type: "backgrounded", info: { id: job.id } })
yield* jobs.cancel(job.id)
}),
{ git: true },
)
}) })
@@ -1448,6 +1448,7 @@ describe("session.message-v2.fromError", () => {
"prompt is too long: 213462 tokens > 200000 maximum", "prompt is too long: 213462 tokens > 200000 maximum",
"Your input exceeds the context window of this model", "Your input exceeds the context window of this model",
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)", "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
"tokens in request more than max tokens allowed",
"Please reduce the length of the messages or completion", "Please reduce the length of the messages or completion",
"400 status code (no body)", "400 status code (no body)",
"413 status code (no body)", "413 status code (no body)",
+4 -3
View File
@@ -150,7 +150,7 @@ describe("tool.registry", () => {
}), }),
) )
it.instance("hides task background parameter unless experimental background subagents are enabled", () => it.instance("exposes the task background parameter by default", () =>
Effect.gen(function* () { Effect.gen(function* () {
const registry = yield* ToolRegistry.Service const registry = yield* ToolRegistry.Service
const agent = yield* Agent.Service const agent = yield* Agent.Service
@@ -162,8 +162,9 @@ describe("tool.registry", () => {
agent: build, agent: build,
})).find((tool) => tool.id === "task") })).find((tool) => tool.id === "task")
expect(task?.jsonSchema).toBeDefined() if (!task) throw new Error("task tool not found")
expect((task?.jsonSchema?.properties as Record<string, unknown> | undefined)?.background).toBeUndefined() const jsonSchema = ToolJsonSchema.fromTool(task)
expect((jsonSchema.properties as Record<string, unknown> | undefined)?.background).toBeDefined()
}), }),
) )
+9 -41
View File
@@ -34,7 +34,7 @@ const ref = {
modelID: ModelV2.ID.make("test-model"), modelID: ModelV2.ID.make("test-model"),
} }
const layer = (flags: Partial<RuntimeFlags.Info> = {}) => const layer = () =>
LayerNode.compile( LayerNode.compile(
LayerNode.group([ LayerNode.group([
Agent.node, Agent.node,
@@ -52,11 +52,10 @@ const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
RuntimeFlags.node, RuntimeFlags.node,
Ripgrep.node, Ripgrep.node,
]), ]),
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]], [[RuntimeFlags.node, RuntimeFlags.layer()]],
) )
const it = testEffect(layer()) const it = testEffect(layer())
const background = testEffect(layer({ experimentalBackgroundSubagents: true }))
function defer<T>() { function defer<T>() {
let resolve!: (value: T | PromiseLike<T>) => void let resolve!: (value: T | PromiseLike<T>) => void
@@ -456,37 +455,6 @@ describe("tool.task", () => {
}, },
) )
it.instance("rejects background execution when the experiment is disabled", () =>
Effect.gen(function* () {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* tool.init()
const exit = yield* def
.execute(
{
description: "inspect bug",
prompt: "look into the cache key path",
subagent_type: "general",
background: true,
},
{
sessionID: chat.id,
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
extra: { promptOps: stubOps() },
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
},
)
.pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
}),
)
it.instance("backgrounds a running foreground task without restarting it", () => it.instance("backgrounds a running foreground task without restarting it", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
@@ -558,7 +526,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("execute launches background tasks without waiting for completion", () => it.instance("execute launches background tasks without waiting for completion", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const { chat, assistant } = yield* seed() const { chat, assistant } = yield* seed()
@@ -596,7 +564,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("running task_id reports the existing background task", () => it.instance("running task_id reports the existing background task", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const { chat, assistant } = yield* seed() const { chat, assistant } = yield* seed()
@@ -661,7 +629,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("background tasks complete through the job service", () => it.instance("background tasks complete through the job service", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const { chat, assistant } = yield* seed() const { chat, assistant } = yield* seed()
@@ -694,7 +662,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("background task completion does not wait for the parent async prompt", () => it.instance("background task completion does not wait for the parent async prompt", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const { chat, assistant } = yield* seed() const { chat, assistant } = yield* seed()
@@ -732,7 +700,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("removing the parent session cancels running background tasks", () => it.instance("removing the parent session cancels running background tasks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const sessions = yield* Session.Service const sessions = yield* Session.Service
@@ -771,7 +739,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("removing the child task session cancels its running background task", () => it.instance("removing the child task session cancels its running background task", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const sessions = yield* Session.Service const sessions = yield* Session.Service
@@ -810,7 +778,7 @@ describe("tool.task", () => {
}), }),
) )
background.instance("cancelling the parent run cancels running background tasks", () => it.instance("cancelling the parent run cancels running background tasks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const jobs = yield* Job.Service const jobs = yield* Job.Service
const runState = yield* SessionRunState.Service const runState = yield* SessionRunState.Service
+1
View File
@@ -475,6 +475,7 @@ export type TuiHostSlotMap = {
} }
sidebar_footer: { sidebar_footer: {
session_id: string session_id: string
directory: string
} }
} }
+1
View File
@@ -61,6 +61,7 @@ export const groupNames = {
} as const } as const
export const endpointNames = { export const endpointNames = {
"debug.location.evict": "evictLocation",
"session.messages": "list", "session.messages": "list",
"integration.connect.key": "connectKey", "integration.connect.key": "connectKey",
"integration.connect.oauth": "connectOauth", "integration.connect.oauth": "connectOauth",
+16 -1
View File
@@ -1,6 +1,7 @@
import { Location } from "@opencode-ai/schema/location" import { Location } from "@opencode-ai/schema/location"
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const DebugGroup = HttpApiGroup.make("server.debug") export const DebugGroup = HttpApiGroup.make("server.debug")
.add( .add(
@@ -14,4 +15,18 @@ export const DebugGroup = HttpApiGroup.make("server.debug")
}), }),
), ),
) )
.add(
HttpApiEndpoint.delete("debug.location.evict", "/api/debug/location", {
query: LocationQuery,
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.debug.location.evict",
summary: "Evict a loaded location",
description: "Dispose the requested location's cached services so its next use boots them fresh.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "debug" })) .annotateMerge(OpenApi.annotations({ title: "debug" }))
+15 -1
View File
@@ -19,4 +19,18 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
}), }),
), ),
) )
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server status routes." })) .add(
HttpApiEndpoint.get("mcp.resource.catalog", "/api/mcp/resource", {
query: LocationQuery,
success: Location.response(Mcp.ResourceCatalog),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.mcp.resource.catalog",
summary: "List MCP resources",
description: "Retrieve resources and resource templates from connected MCP servers.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server and resource routes." }))
+20
View File
@@ -269,6 +269,25 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
}), }),
), ),
) )
.add(
HttpApiEndpoint.post("session.move", "/api/session/:sessionID/move", {
params: { sessionID: Session.ID },
payload: Schema.Struct({
destination: Schema.Struct({ directory: AbsolutePath }),
moveChanges: Schema.Boolean.pipe(Schema.optional),
}),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, InvalidRequestError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.move",
summary: "Move session",
description: "Move a session to another project directory, optionally transferring local changes.",
}),
),
)
.add( .add(
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", { HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
params: { sessionID: Session.ID }, params: { sessionID: Session.ID },
@@ -344,6 +363,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
text: Schema.String, text: Schema.String,
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),
metadata: SessionMessage.Synthetic.fields.metadata, metadata: SessionMessage.Synthetic.fields.metadata,
resume: Schema.Boolean.pipe(Schema.optional),
}), }),
success: HttpApiSchema.NoContent, success: HttpApiSchema.NoContent,
error: SessionNotFoundError, error: SessionNotFoundError,
+1
View File
@@ -4,5 +4,6 @@ import { isOpenCodeEvent } from "../src/groups/event.js"
test("classifies public events by type", () => { test("classifies public events by type", () => {
expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true) expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true)
expect(isOpenCodeEvent({ type: "mcp.status.changed" })).toBe(true) expect(isOpenCodeEvent({ type: "mcp.status.changed" })).toBe(true)
expect(isOpenCodeEvent({ type: "mcp.resources.changed" })).toBe(true)
expect(isOpenCodeEvent({ type: "mcp.tools.changed" })).toBe(false) expect(isOpenCodeEvent({ type: "mcp.tools.changed" })).toBe(false)
}) })
+1
View File
@@ -80,6 +80,7 @@ export const ServerDefinitions = Event.inventory(
...InstallationEvent.Definitions, ...InstallationEvent.Definitions,
...VcsEvent.Definitions, ...VcsEvent.Definitions,
McpEvent.StatusChanged, McpEvent.StatusChanged,
McpEvent.ResourcesChanged,
// Shared transitional: V1 contracts the current TUI still consumes during // Shared transitional: V1 contracts the current TUI still consumes during
// the migration (permission.asked/replied, question.asked, session.error). // the migration (permission.asked/replied, question.asked, session.error).
// Remove when the TUI moves to the current permission/question surfaces. // Remove when the TUI moves to the current permission/question surfaces.
+1
View File
@@ -9,6 +9,7 @@ export { Form } from "./form.js"
export { Integration } from "./integration.js" export { Integration } from "./integration.js"
export { LLM } from "./llm.js" export { LLM } from "./llm.js"
export { Location } from "./location.js" export { Location } from "./location.js"
export { Mcp } from "./mcp.js"
export { Model } from "./model.js" export { Model } from "./model.js"
export { Permission } from "./permission.js" export { Permission } from "./permission.js"
export { PermissionSaved } from "./permission-saved.js" export { PermissionSaved } from "./permission-saved.js"
+8 -1
View File
@@ -10,6 +10,13 @@ export const ToolsChanged = Event.ephemeral({
}, },
}) })
export const ResourcesChanged = Event.ephemeral({
type: "mcp.resources.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = Event.ephemeral({ export const BrowserOpenFailed = Event.ephemeral({
type: "mcp.browser.open.failed", type: "mcp.browser.open.failed",
schema: { schema: {
@@ -27,4 +34,4 @@ export const StatusChanged = Event.ephemeral({
}, },
}) })
export const Definitions = Event.inventory(ToolsChanged, StatusChanged) export const Definitions = Event.inventory(ToolsChanged, ResourcesChanged, StatusChanged)
+50 -8
View File
@@ -25,14 +25,9 @@ const NeedsClientRegistration = Schema.Struct({
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" }) }).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
export type Status = typeof Status.Type export type Status = typeof Status.Type
export const Status = Schema.Union([ export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
Connected, Schema.toTaggedUnion("status"),
Pending, )
Disabled,
Failed,
NeedsAuth,
NeedsClientRegistration,
]).pipe(Schema.toTaggedUnion("status"))
export interface Server extends Schema.Schema.Type<typeof Server> {} export interface Server extends Schema.Schema.Type<typeof Server> {}
export const Server = Schema.Struct({ export const Server = Schema.Struct({
@@ -42,3 +37,50 @@ export const Server = Schema.Struct({
// without matching by name, which could collide with provider or plugin integrations. // without matching by name, which could collide with provider or plugin integrations.
integrationID: optional(IntegrationID), integrationID: optional(IntegrationID),
}).annotate({ identifier: "Mcp.Server" }) }).annotate({ identifier: "Mcp.Server" })
export interface Resource extends Schema.Schema.Type<typeof Resource> {}
export const Resource = Schema.Struct({
server: Schema.String,
name: Schema.String,
uri: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.Resource" })
export interface ResourceTemplate extends Schema.Schema.Type<typeof ResourceTemplate> {}
export const ResourceTemplate = Schema.Struct({
server: Schema.String,
name: Schema.String,
uriTemplate: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.ResourceTemplate" })
export interface ResourceCatalog extends Schema.Schema.Type<typeof ResourceCatalog> {}
export const ResourceCatalog = Schema.Struct({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}).annotate({ identifier: "Mcp.ResourceCatalog" })
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: optional(Schema.String),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: optional(Schema.String),
}),
]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Mcp.ResourceContentPart" }))
export type ResourceContentPart = typeof ResourceContentPart.Type
export interface ResourceContent extends Schema.Schema.Type<typeof ResourceContent> {}
export const ResourceContent = Schema.Struct({
server: Schema.String,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}).annotate({ identifier: "Mcp.ResourceContent" })
+23 -9
View File
@@ -30,6 +30,15 @@ export interface Source extends Schema.Schema.Type<typeof Source> {}
const Base = { const Base = {
sessionID: SessionID, sessionID: SessionID,
} }
const Tokens = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
})
const PromptFields = { const PromptFields = {
...Base, ...Base,
inputID: SessionMessage.ID, inputID: SessionMessage.ID,
@@ -84,6 +93,16 @@ export const Renamed = Event.durable({
}) })
export type Renamed = typeof Renamed.Type export type Renamed = typeof Renamed.Type
export const UsageUpdated = Event.ephemeral({
type: "session.usage.updated",
schema: {
...Base,
cost: Schema.Finite,
tokens: Tokens,
},
})
export type UsageUpdated = typeof UsageUpdated.Type
export const Deleted = Event.durable({ export const Deleted = Event.durable({
type: "session.deleted", type: "session.deleted",
durable: { durable: {
@@ -224,15 +243,7 @@ export namespace Step {
assistantMessageID: SessionMessage.ID, assistantMessageID: SessionMessage.ID,
finish: FinishReason, finish: FinishReason,
cost: Schema.Finite, cost: Schema.Finite,
tokens: Schema.Struct({ tokens: Tokens,
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
snapshot: Schema.String.pipe(optional), snapshot: Schema.String.pipe(optional),
files: Schema.Array(RelativePath).pipe(optional), files: Schema.Array(RelativePath).pipe(optional),
}, },
@@ -246,6 +257,8 @@ export namespace Step {
...Base, ...Base,
assistantMessageID: SessionMessage.ID, assistantMessageID: SessionMessage.ID,
error: SessionError.Error, error: SessionError.Error,
cost: Schema.Finite.pipe(optional),
tokens: Tokens.pipe(optional),
}, },
}) })
export type Failed = typeof Failed.Type export type Failed = typeof Failed.Type
@@ -504,6 +517,7 @@ export const Definitions = Event.inventory(
ModelSelected, ModelSelected,
Moved, Moved,
Renamed, Renamed,
UsageUpdated,
Deleted, Deleted,
Forked, Forked,
PromptPromoted, PromptPromoted,
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { DateTime, Schema } from "effect" import { DateTime, Schema } from "effect"
import { Agent } from "../src/agent.js" import { Agent } from "../src/agent.js"
import { FileSystem } from "../src/filesystem.js" import { FileSystem } from "../src/filesystem.js"
import { Mcp } from "../src/mcp.js"
import { Model } from "../src/model.js" import { Model } from "../src/model.js"
import { Project } from "../src/project.js" import { Project } from "../src/project.js"
import { Provider } from "../src/provider.js" import { Provider } from "../src/provider.js"
@@ -51,6 +52,11 @@ describe("contract hygiene", () => {
const identifiers = [ const identifiers = [
Agent.Color, Agent.Color,
FileSystem.Submatch, FileSystem.Submatch,
Mcp.Resource,
Mcp.ResourceTemplate,
Mcp.ResourceCatalog,
Mcp.ResourceContentPart,
Mcp.ResourceContent,
Model.Ref, Model.Ref,
Model.Capabilities, Model.Capabilities,
Model.Cost, Model.Cost,
+4 -1
View File
@@ -51,6 +51,7 @@ describe("public event manifest", () => {
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated) expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("plugin.updated")).toBe(Plugin.Event.Updated) expect(EventManifest.Latest.get("plugin.updated")).toBe(Plugin.Event.Updated)
expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged) expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged)
expect(EventManifest.Server.get("mcp.resources.changed")).toBe(McpEvent.ResourcesChanged)
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted) expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false) expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
expect(Agent.Event.Updated.durable).toBeUndefined() expect(Agent.Event.Updated.durable).toBeUndefined()
@@ -76,7 +77,7 @@ describe("public event manifest", () => {
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled]) expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated]) expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated]) expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.StatusChanged]) expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false) expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
expect(EventManifest.Latest.has("ide.installed")).toBe(false) expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed]) expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
@@ -143,6 +144,8 @@ describe("public event manifest", () => {
expect(SessionEvent.DurableDefinitions).toEqual( expect(SessionEvent.DurableDefinitions).toEqual(
SessionEvent.Definitions.filter((definition) => definition.durability === "durable"), SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
) )
expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral")
expect(EventManifest.ServerDefinitions).toContain(SessionEvent.UsageUpdated)
expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true) expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true)
}) })
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { Mcp } from "../src/mcp.js"
describe("Mcp resources", () => {
test("decodes resource catalogs and omits absent metadata", () => {
const value = Schema.decodeUnknownSync(Mcp.ResourceCatalog)({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
expect(Schema.encodeSync(Mcp.ResourceCatalog)(value)).toEqual({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
})
test("preserves text and base64 blob contents", () => {
expect(
Schema.decodeUnknownSync(Mcp.ResourceContent)({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
}),
).toEqual({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
})
})
})

Some files were not shown because too many files have changed in this diff Show More