refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
commit 8db7487c89
466 changed files with 9405 additions and 11071 deletions

View file

@ -184,7 +184,7 @@
"effect": "catalog:", "effect": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"effect": "4.0.0-beta.98", "effect": "4.0.0-beta.101",
}, },
"optionalPeers": [ "optionalPeers": [
"effect", "effect",
@ -376,6 +376,7 @@
"@openrouter/ai-sdk-provider": "2.9.0", "@openrouter/ai-sdk-provider": "2.9.0",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4", "@silvia-odwyer/photon-node": "0.3.4",
"@standard-schema/spec": "catalog:",
"ai-gateway-provider": "3.1.2", "ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8", "bun-pty": "0.4.8",
"diff": "catalog:", "diff": "catalog:",
@ -546,7 +547,7 @@
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.18.4", "version": "1.18.4",
"dependencies": { "dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.98", "@effect/platform-node-shared": "4.0.0-beta.101",
}, },
"devDependencies": { "devDependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@ -583,7 +584,7 @@
"@opencode-ai/client": "workspace:*", "@opencode-ai/client": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "1.18.5", "@opencode-ai/sdk": "1.18.5",
"@standard-schema/spec": "^1.1.0", "@standard-schema/spec": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
@ -626,6 +627,7 @@
"name": "@opencode-ai/schema", "name": "@opencode-ai/schema",
"version": "1.17.11", "version": "1.17.11",
"dependencies": { "dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:", "effect": "catalog:",
}, },
"devDependencies": { "devDependencies": {
@ -1042,16 +1044,16 @@
], ],
"patchedDependencies": { "patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch",
}, },
"overrides": { "overrides": {
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
@ -1059,13 +1061,14 @@
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"effect": "catalog:",
}, },
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@cloudflare/workers-types": "4.20251008.0",
"@corvu/drawer": "0.2.4", "@corvu/drawer": "0.2.4",
"@effect/opentelemetry": "4.0.0-beta.98", "@effect/opentelemetry": "4.0.0-beta.101",
"@effect/platform-node": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.101",
"@effect/sql-sqlite-bun": "4.0.0-beta.98", "@effect/sql-sqlite-bun": "4.0.0-beta.101",
"@hono/standard-validator": "0.2.0", "@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
@ -1085,6 +1088,7 @@
"@solidjs/meta": "0.29.4", "@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4", "@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@standard-schema/spec": "1.1.0",
"@tailwindcss/vite": "4.1.11", "@tailwindcss/vite": "4.1.11",
"@tanstack/solid-virtual": "3.13.32", "@tanstack/solid-virtual": "3.13.32",
"@tsconfig/bun": "1.0.9", "@tsconfig/bun": "1.0.9",
@ -1101,7 +1105,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.98", "effect": "4.0.0-beta.101",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"get-east-asian-width": "1.6.0", "get-east-asian-width": "1.6.0",
"hono": "4.10.7", "hono": "4.10.7",
@ -1545,13 +1549,13 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.98", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.98" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-ITfK8xhcl+9GXOvPwzADWkOQ+dgUGZrJNefT3r2+uLFmzjyKRLtHzhLOl6lZaLSsf5io13+nmt8adfMRQPq+oA=="], "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.101", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.101" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-IdejlqRLbjRHJgVnea4s8CxTWfvkSjM0HlnpNfP07IGTbhmAvPs7PMaWt1xzYWbOnI7CriQwxv+54eW1PwIkZg=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.101", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.101", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101", "ioredis": "^5.7.0" } }, "sha512-pClk7dmMtHgM6Byu7CzGfrPvZ1/4BwmrRlCg2Op+iJozMkwVUxq8v4beK8b9SvxsliJjHZFznjvkVLX7LQjBqw=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.101", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-g4L7XiyJSNJLJVhlslyg2zBCQsoKQf1y1gd+Yfd+3wD9ymC+m7ymbd/5FGqnT1aXV6E2AwRr4D/R1eyRUikvWQ=="],
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="], "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@ -2105,8 +2109,6 @@
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="],
@ -3829,7 +3831,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], "effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],

View file

@ -36,9 +36,9 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@effect/opentelemetry": "4.0.0-beta.98", "@effect/opentelemetry": "4.0.0-beta.101",
"@effect/platform-node": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.101",
"@effect/sql-sqlite-bun": "4.0.0-beta.98", "@effect/sql-sqlite-bun": "4.0.0-beta.101",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13", "@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
@ -50,6 +50,7 @@
"@opentui/solid": "0.4.5", "@opentui/solid": "0.4.5",
"@tanstack/solid-virtual": "3.13.32", "@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0", "@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0",
"ulid": "3.0.1", "ulid": "3.0.1",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@corvu/drawer": "0.2.4", "@corvu/drawer": "0.2.4",
@ -68,7 +69,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.98", "effect": "4.0.0-beta.101",
"ai": "6.0.168", "ai": "6.0.168",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
@ -151,7 +152,8 @@
"@opentui/keymap": "catalog:", "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:" "@types/node": "catalog:",
"effect": "catalog:"
}, },
"patchedDependencies": { "patchedDependencies": {
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
@ -165,7 +167,7 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
} }
} }

View file

@ -1,4 +1,5 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client" import { Route } from "../route/client"
import { Auth } from "../route/auth" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint" import { Endpoint } from "../route/endpoint"
@ -19,7 +20,6 @@ import {
type ProviderMetadata, type ProviderMetadata,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolContent,
type ToolResultPart, type ToolResultPart,
} from "../schema" } from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
@ -425,7 +425,7 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me
// Tool results may carry structured text, images, and documents. Keep media as provider-native // Tool results may carry structured text, images, and documents. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string. // content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
item: ToolContent, item: Tool.Content,
) { ) {
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }) return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
@ -436,7 +436,7 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte
// with existing cassettes and provider expectations. // with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part) if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package. // Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<ToolContent> = part.result.value const content: ReadonlyArray<Tool.Content> = part.result.value
return yield* Effect.forEach(content, lowerToolResultContentItem) return yield* Effect.forEach(content, lowerToolResultContentItem)
}) })

View file

@ -1,4 +1,5 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client" import { Route } from "../route/client"
import { Auth } from "../route/auth" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint" import { Endpoint } from "../route/endpoint"
@ -16,7 +17,6 @@ import {
type TextPart, type TextPart,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolContent,
} from "../schema" } from "../schema"
import { JsonObject, optionalArray, ProviderShared } from "./shared" import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { GeminiToolSchema } from "./utils/gemini-tool-schema" import { GeminiToolSchema } from "./utils/gemini-tool-schema"
@ -289,7 +289,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
}) })
continue continue
} }
const content: ReadonlyArray<ToolContent> = part.result.value const content: ReadonlyArray<Tool.Content> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text) const text = content.filter((item) => item.type === "text").map((item) => item.text)
const media: GeminiInlineDataPart[] = [] const media: GeminiInlineDataPart[] = []
for (const item of content) { for (const item of content) {

View file

@ -1,4 +1,5 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import type { Content } from "@opencode-ai/schema/tool"
import { HttpTransport } from "../route/transport" import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol" import { Protocol } from "../route/protocol"
import { import {
@ -14,7 +15,6 @@ import {
type TextPart, type TextPart,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolContent,
type ToolResultPart, type ToolResultPart,
} from "../schema" } from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
@ -371,7 +371,7 @@ const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* (
// Tool results may carry structured text, images, and files. Keep media as provider-native // Tool results may carry structured text, images, and files. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string. // content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* ( const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (
item: ToolContent, item: Content,
request: LLMRequest, request: LLMRequest,
extension: Extension, extension: Extension,
) { ) {
@ -392,7 +392,7 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f
// compatibility with existing cassettes and provider expectations. // compatibility with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part) if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package. // Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<ToolContent> = part.result.value const content: ReadonlyArray<Content> = part.result.value
return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)) return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension))
}) })
@ -496,7 +496,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
if (store !== false && itemID && !hostedToolReferences.has(itemID)) if (store !== false && itemID && !hostedToolReferences.has(itemID))
input.push({ type: "item_reference", id: itemID }) input.push({ type: "item_reference", id: itemID })
if (store === false && part.result.type === "content") { if (store === false && part.result.type === "content") {
const content: ReadonlyArray<ToolContent> = part.result.value const content: ReadonlyArray<Content> = part.result.value
input.push({ input.push({
role: "user", role: "user",
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)), content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),

View file

@ -1,4 +1,5 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client" import { Route } from "../route/client"
import { Auth } from "../route/auth" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint" import { Endpoint } from "../route/endpoint"
@ -17,7 +18,6 @@ import {
type TextPart, type TextPart,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolContent,
} from "../schema" } from "../schema"
import { classifyProviderFailure } from "../provider-error" import { classifyProviderFailure } from "../provider-error"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
@ -335,7 +335,7 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m
messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) }) messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) })
continue continue
} }
const content: ReadonlyArray<ToolContent> = part.result.value const content: ReadonlyArray<Tool.Content> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text) const text = content.filter((item) => item.type === "text").map((item) => item.text)
messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") }) messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") })
const files = content.filter((item) => item.type === "file") const files = content.filter((item) => item.type === "file")

View file

@ -1,4 +1,5 @@
import { Buffer } from "node:buffer" import { Buffer } from "node:buffer"
import { Tool } from "@opencode-ai/schema/tool"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema, Stream } from "effect"
import * as Sse from "effect/unstable/encoding/Sse" import * as Sse from "effect/unstable/encoding/Sse"
import { Headers, HttpClientRequest } from "effect/unstable/http" import { Headers, HttpClientRequest } from "effect/unstable/http"
@ -9,7 +10,6 @@ import {
type ContentPart, type ContentPart,
type LLMRequest, type LLMRequest,
type MediaPart, type MediaPart,
type ToolFileContent,
type TextPart, type TextPart,
type ToolResultPart, type ToolResultPart,
} from "../schema" } from "../schema"
@ -206,7 +206,7 @@ export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function*
return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia
}) })
export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet<string>) => export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes) validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")

View file

@ -1,4 +1,5 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids" import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
export const ProviderFailureClassification = Schema.Literal("context-overflow") export const ProviderFailureClassification = Schema.Literal("context-overflow")
@ -152,8 +153,4 @@ export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
* Anything thrown or yielded by a handler that is not a `ToolFailure` is * Anything thrown or yielded by a handler that is not a `ToolFailure` is
* treated as a defect and fails the stream. * treated as a defect and fails the stream.
*/ */
export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", { export class ToolFailure extends Tool.Error {}
message: Schema.String,
error: Schema.optional(Schema.Defect()),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {}

View file

@ -1,5 +1,5 @@
import { Schema } from "effect" import { Schema } from "effect"
import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm" import { Tool } from "@opencode-ai/schema/tool"
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
import { isRecord } from "../utils/record" import { isRecord } from "../utils/record"
@ -40,8 +40,6 @@ export const MediaPart = Schema.Struct({
}).annotate({ identifier: "LLM.Content.Media" }) }).annotate({ identifier: "LLM.Content.Media" })
export type MediaPart = Schema.Schema.Type<typeof MediaPart> export type MediaPart = Schema.Schema.Type<typeof MediaPart>
export { ToolContent, ToolFileContent, ToolTextContent }
const isToolResultValue = (value: unknown): value is ToolResultValue => const isToolResultValue = (value: unknown): value is ToolResultValue =>
isRecord(value) && isRecord(value) &&
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
@ -63,7 +61,7 @@ export const ToolResultValue = Object.assign(
}), }),
Schema.Struct({ Schema.Struct({
type: Schema.Literal("content"), type: Schema.Literal("content"),
value: Schema.Array(ToolContent), value: Schema.Array(Tool.Content),
}), }),
]).annotate({ identifier: "LLM.ToolResult" }), ]).annotate({ identifier: "LLM.ToolResult" }),
{ {
@ -79,16 +77,16 @@ export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
export interface ToolOutput { export interface ToolOutput {
readonly structured: unknown readonly structured: unknown
readonly content: ReadonlyArray<ToolContent> readonly content: ReadonlyArray<Tool.Content>
} }
export const ToolOutput = Object.assign( export const ToolOutput = Object.assign(
Schema.Struct({ Schema.Struct({
structured: Schema.Unknown, structured: Schema.Unknown,
content: Schema.Array(ToolContent), content: Schema.Array(Tool.Content),
}).annotate({ identifier: "LLM.ToolOutput" }), }).annotate({ identifier: "LLM.ToolOutput" }),
{ {
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({ structured, content }), make: (structured: unknown, content: ReadonlyArray<Tool.Content> = []): ToolOutput => ({ structured, content }),
fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
switch (result.type) { switch (result.type) {
case "json": case "json":

View file

@ -28,7 +28,7 @@ export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<Dispat
return decodeAndExecute(tool, call).pipe( return decodeAndExecute(tool, call).pipe(
Effect.map((value) => result(call, value)), Effect.map((value) => result(call, value)),
Effect.catchTag("LLM.ToolFailure", (failure) => Effect.catchTag("Tool.Error", (failure) =>
Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)), Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)),
), ),
) )

View file

@ -1,7 +1,7 @@
import { Effect, JsonSchema, Schema } from "effect" import { Effect, JsonSchema, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import type { import type {
ToolCallPart, ToolCallPart,
ToolContent,
ToolDefinition as ToolDefinitionClass, ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType, ToolOutput as ToolOutputType,
} from "./schema" } from "./schema"
@ -31,7 +31,7 @@ export interface ToolModelOutputInput<Parameters, Output> {
export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = ( export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>, input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>,
) => ReadonlyArray<ToolContent> ) => ReadonlyArray<Tool.Content>
/** /**
* A type-safe LLM tool. Each tool bundles its own description, parameter * A type-safe LLM tool. Each tool bundles its own description, parameter
@ -95,7 +95,7 @@ type DynamicToolConfig = {
readonly jsonSchema: JsonSchema.JsonSchema readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure> readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toStructuredOutput?: (output: unknown) => unknown readonly toStructuredOutput?: (output: unknown) => unknown
} }
@ -151,7 +151,7 @@ export function make(config: {
readonly jsonSchema: JsonSchema.JsonSchema readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema
readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure> readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toStructuredOutput?: (output: unknown) => unknown readonly toStructuredOutput?: (output: unknown) => unknown
}): AnyExecutableTool }): AnyExecutableTool
export function make(config: { export function make(config: {
@ -159,7 +159,7 @@ export function make(config: {
readonly jsonSchema: JsonSchema.JsonSchema readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: undefined readonly execute?: undefined
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toStructuredOutput?: (output: unknown) => unknown readonly toStructuredOutput?: (output: unknown) => unknown
}): AnyTool }): AnyTool
export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
@ -236,7 +236,7 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
} }
const project = ( const project = (
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<ToolContent>) | undefined, toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<Tool.Content>) | undefined,
toStructuredOutput: ((output: unknown) => unknown) | undefined, toStructuredOutput: ((output: unknown) => unknown) | undefined,
parameters: unknown, parameters: unknown,
callID: ToolCallPart["id"], callID: ToolCallPart["id"],

View file

@ -1,4 +1,5 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Content } from "@opencode-ai/schema/tool"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema, Stream } from "effect"
import { import {
GenerationOptions, GenerationOptions,
@ -7,7 +8,6 @@ import {
LLMRequest, LLMRequest,
LLMResponse, LLMResponse,
ToolChoice, ToolChoice,
ToolContent,
ToolOutput, ToolOutput,
toDefinitions, toDefinitions,
} from "../src" } from "../src"
@ -279,7 +279,7 @@ describe("LLMClient tools", () => {
it.effect("models canonical tool files with URIs", () => it.effect("models canonical tool files with URIs", () =>
Effect.sync(() => { Effect.sync(() => {
const decode = Schema.decodeUnknownSync(ToolContent) const decode = Schema.decodeUnknownSync(Content)
expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({ expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({
type: "file", type: "file",

View file

@ -25,7 +25,7 @@ describe("adaptServerEvent", () => {
} as OpenCodeEvent } as OpenCodeEvent
expect(adaptServerEvent(current)).toMatchObject({ expect(adaptServerEvent(current)).toMatchObject({
type: "permission.asked", type: "permission.v2.asked",
properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] },
current, current,
}) })

View file

@ -29,7 +29,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
if (event.type === "permission.v2.asked") { if (event.type === "permission.v2.asked") {
return { return {
id: event.id, id: event.id,
type: "permission.asked", type: "permission.v2.asked",
properties: { properties: {
id: event.data.id, id: event.data.id,
sessionID: event.data.sessionID, sessionID: event.data.sessionID,
@ -46,13 +46,13 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
} as ServerEvent } as ServerEvent
} }
if (event.type === "permission.v2.replied") if (event.type === "permission.v2.replied")
return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent return { id: event.id, type: "permission.v2.replied", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.asked") if (event.type === "question.v2.asked")
return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent return { id: event.id, type: "question.v2.asked", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.replied") if (event.type === "question.v2.replied")
return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent return { id: event.id, type: "question.v2.replied", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.rejected") if (event.type === "question.v2.rejected")
return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent return { id: event.id, type: "question.v2.rejected", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent
} }

View file

@ -71,7 +71,7 @@ export async function streamTurn(input: {
const next = await stream.next() const next = await stream.next()
if (next.done) throw new Error("event stream disconnected during prompt execution") if (next.done) throw new Error("event stream disconnected during prompt execution")
const event = next.value const event = next.value
if (event.type === "permission.v2.asked" && event.data.sessionID === input.sessionID) { if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) {
const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined
await replyPermission({ await replyPermission({
client: input.client, client: input.client,

View file

@ -5,7 +5,7 @@ import { Result } from "effect"
import { isAbsolute, resolve } from "node:path" import { isAbsolute, resolve } from "node:path"
import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool" import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool"
type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.v2.asked" }> type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.asked" }>
type Connection = Pick<AgentSideConnection, "requestPermission"> & Partial<Pick<AgentSideConnection, "writeTextFile">> type Connection = Pick<AgentSideConnection, "requestPermission"> & Partial<Pick<AgentSideConnection, "writeTextFile">>
type Tool = { readonly name: string; readonly input: ToolInput } type Tool = { readonly name: string; readonly input: ToolInput }

View file

@ -9,8 +9,8 @@ import {
Provider, Provider,
Reference, Reference,
Skill, Skill,
} from "@opencode-ai/plugin/v2/effect" } from "@opencode-ai/plugin/effect"
import { Tool } from "@opencode-ai/plugin/v2/effect/tool" import { Tool } from "@opencode-ai/schema/tool"
const key = Symbol.for("opencode.plugin.v2.effect") const key = Symbol.for("opencode.plugin.v2.effect")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
@ -24,5 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.effect")
Provider, Provider,
Reference, Reference,
Skill, Skill,
Tool, Tool: { Error: Tool.Error },
} }

View file

@ -9,8 +9,7 @@ import {
Provider, Provider,
Reference, Reference,
Skill, Skill,
} from "@opencode-ai/plugin/v2" } from "@opencode-ai/plugin"
import { Tool } from "@opencode-ai/plugin/v2/tool"
const key = Symbol.for("opencode.plugin.v2.promise") const key = Symbol.for("opencode.plugin.v2.promise")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
@ -24,5 +23,4 @@ const key = Symbol.for("opencode.plugin.v2.promise")
Provider, Provider,
Reference, Reference,
Skill, Skill,
Tool,
} }

View file

@ -177,7 +177,7 @@ export async function runNonInteractivePrompt(input: Input) {
} }
const event = next.value const event = next.value
if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) { if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) {
await replyPermission(event.data) await replyPermission(event.data)
continue continue
} }

View file

@ -13,11 +13,13 @@ type Options = {
readonly command?: ReadonlyArray<string> readonly command?: ReadonlyArray<string>
} }
const startupDirectory = process.cwd()
function command(password: string, options: Options) { function command(password: string, options: Options) {
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"] const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
if (!executable) throw new Error("Failed to resolve standalone server command") if (!executable) throw new Error("Failed to resolve standalone server command")
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], { return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
cwd: process.cwd(), cwd: startupDirectory,
// Explicit entry wins over anything inherited, so a user-exported // Explicit entry wins over anything inherited, so a user-exported
// OPENCODE_PASSWORD cannot shadow the child's lease credential. // OPENCODE_PASSWORD cannot shadow the child's lease credential.
env: { OPENCODE_PASSWORD: password }, env: { OPENCODE_PASSWORD: password },

View file

@ -1,11 +1,13 @@
import path from "node:path" import path from "node:path"
const entrypoint = process.argv[1] ? path.resolve(process.argv[1]) : undefined
export function selfCommand() { export function selfCommand() {
const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase() const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase()
if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath] if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath]
if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint") if (!entrypoint) throw new Error("Failed to resolve CLI entrypoint")
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]] if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), entrypoint]
return [process.execPath, process.argv[1]] return [process.execPath, entrypoint]
} }
function nodeFlags() { function nodeFlags() {

View file

@ -502,7 +502,7 @@ function permissionAsked(
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
} = {}, } = {},
) { ) {
return ephemeralEvent("permission.v2.asked", { return ephemeralEvent("permission.asked", {
id, id,
sessionID, sessionID,
action: input.action ?? "shell", action: input.action ?? "shell",

View file

@ -3,12 +3,14 @@ import { Service } from "@opencode-ai/client/effect/service"
import path from "node:path" import path from "node:path"
import { Standalone } from "../../src/services/standalone" import { Standalone } from "../../src/services/standalone"
process.argv[1] = path.join(import.meta.dir, "../../src/index.ts") process.chdir(path.join(import.meta.dir, "../../../.."))
await Effect.runPromise( await Effect.runPromise(
Effect.scoped( Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
const endpoint = yield* Standalone.start() const endpoint = yield* Standalone.start({
command: [process.execPath, path.join(import.meta.dir, "../../src/index.ts"), "serve"],
})
const response = yield* Effect.promise(() => const response = yield* Effect.promise(() =>
fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }), fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }),
) )

View file

@ -75,20 +75,11 @@ export const define = sdk.Plugin.define`
const effectPluginModule = promisePluginModule const effectPluginModule = promisePluginModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin") .replace("Promise plugin", "Effect plugin")
const promiseToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] const promiseToolModule = `export {}`
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
export const Tool = sdk.Tool
export const make = sdk.Tool.make`
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")] const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable") if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
export const Tool = sdk.Tool export const Error = sdk.Tool.Error
export const Failure = sdk.Tool.Failure `
export const RegistrationError = sdk.Tool.RegistrationError
export const make = sdk.Tool.make
export const validateName = sdk.Tool.validateName
export const registrationEntries = sdk.Tool.registrationEntries
export const validateNamespace = sdk.Tool.validateNamespace
export const toLLMDefinition = sdk.Tool.toLLMDefinition`
return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")} return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
import __cjs_mod__ from "node:module" import __cjs_mod__ from "node:module"
import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs" import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
@ -100,17 +91,17 @@ const __filename = import.meta.filename
const __dirname = import.meta.dirname const __dirname = import.meta.dirname
const require = __cjs_mod__.createRequire(import.meta.url) const require = __cjs_mod__.createRequire(import.meta.url)
const __ocPluginModules = ${JSON.stringify({ const __ocPluginModules = ${JSON.stringify({
"@opencode-ai/plugin/v2": "opencode:plugin-v2", "@opencode-ai/plugin": "opencode:plugin-v2",
"@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin", "@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin",
"@opencode-ai/plugin/v2/tool": "opencode:plugin-v2-tool", "@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool",
"@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect", "@opencode-ai/plugin/effect": "opencode:plugin-v2-effect",
"@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin", "@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin",
"@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool", "@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool",
})} })}
const __ocPluginSources = ${JSON.stringify({ const __ocPluginSources = ${JSON.stringify({
"opencode:plugin-v2": promiseModule, "opencode:plugin-v2": promiseModule,
"opencode:plugin-v2-plugin": promisePluginModule, "opencode:plugin-promise-plugin": promisePluginModule,
"opencode:plugin-v2-tool": promiseToolModule, "opencode:plugin-promise-tool": promiseToolModule,
"opencode:plugin-v2-effect": effectModule, "opencode:plugin-v2-effect": effectModule,
"opencode:plugin-v2-effect-plugin": effectPluginModule, "opencode:plugin-v2-effect-plugin": effectPluginModule,
"opencode:plugin-v2-effect-tool": effectToolModule, "opencode:plugin-v2-effect-tool": effectToolModule,

View file

@ -36,7 +36,7 @@
"@opencode-ai/protocol": "workspace:*" "@opencode-ai/protocol": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"effect": "4.0.0-beta.98" "effect": "4.0.0-beta.101"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"effect": { "effect": {

View file

@ -6,11 +6,86 @@ import {
groupNames, groupNames,
promiseOmitEndpoints, promiseOmitEndpoints,
} from "@opencode-ai/protocol/client" } from "@opencode-ai/protocol/client"
import { Effect } from "effect" import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command"
import { Credential } from "@opencode-ai/schema/credential"
import { Event } from "@opencode-ai/schema/event"
import { EventLog } from "@opencode-ai/schema/event-log"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Form } from "@opencode-ai/schema/form"
import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
import { Mcp } from "@opencode-ai/schema/mcp"
import { Model } from "@opencode-ai/schema/model"
import { Permission } from "@opencode-ai/schema/permission"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Project } from "@opencode-ai/schema/project"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
import { AgentAttachment, FileAttachment, Prompt, PromptMention } from "@opencode-ai/schema/prompt"
import { PromptInput } from "@opencode-ai/schema/prompt-input"
import { Provider } from "@opencode-ai/schema/provider"
import { Pty } from "@opencode-ai/schema/pty"
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
import { Question } from "@opencode-ai/schema/question"
import { Reference } from "@opencode-ai/schema/reference"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
import { Session } from "@opencode-ai/schema/session"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { SessionPending } from "@opencode-ai/schema/session-pending"
import { Shell } from "@opencode-ai/schema/shell"
import { Skill } from "@opencode-ai/schema/skill"
import { Vcs } from "@opencode-ai/schema/vcs"
import { WebSearch } from "@opencode-ai/schema/websearch"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Effect, Schema } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
const effectTypeReferences = [
...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent),
...namespaceTypes("Command", "@opencode-ai/schema/command", Command),
...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential),
...namespaceTypes("Event", "@opencode-ai/schema/event", Event),
...namespaceTypes("EventLog", "@opencode-ai/schema/event-log", EventLog),
...namespaceTypes("FileDiff", "@opencode-ai/schema/file-diff", FileDiff),
...namespaceTypes("FileSystem", "@opencode-ai/schema/filesystem", FileSystem),
...namespaceTypes("Form", "@opencode-ai/schema/form", Form),
...namespaceTypes("InstructionEntry", "@opencode-ai/schema/instruction-entry", InstructionEntry),
...namespaceTypes("Integration", "@opencode-ai/schema/integration", Integration),
...namespaceTypes("Location", "@opencode-ai/schema/location", Location),
...namespaceTypes("Mcp", "@opencode-ai/schema/mcp", Mcp),
...namespaceTypes("Model", "@opencode-ai/schema/model", Model),
...namespaceTypes("Permission", "@opencode-ai/schema/permission", Permission),
...namespaceTypes("PermissionSaved", "@opencode-ai/schema/permission-saved", PermissionSaved),
...namespaceTypes("Plugin", "@opencode-ai/schema/plugin", Plugin),
...namespaceTypes("Project", "@opencode-ai/schema/project", Project),
...namespaceTypes("ProjectCopy", "@opencode-ai/schema/project-copy", ProjectCopy),
...namespaceTypes("PromptInput", "@opencode-ai/schema/prompt-input", PromptInput),
...namespaceTypes("Provider", "@opencode-ai/schema/provider", Provider),
...namespaceTypes("Pty", "@opencode-ai/schema/pty", Pty),
...namespaceTypes("PtyTicket", "@opencode-ai/schema/pty-ticket", PtyTicket),
...namespaceTypes("Question", "@opencode-ai/schema/question", Question),
...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference),
...namespaceTypes("Session", "@opencode-ai/schema/session", Session),
...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage),
...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending),
...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell),
...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill),
...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs),
...namespaceTypes("WebSearch", "@opencode-ai/schema/websearch", WebSearch),
...namespaceTypes("Workspace", "@opencode-ai/schema/workspace", Workspace),
typeReference("Prompt", "@opencode-ai/schema/prompt", Prompt),
typeReference("PromptMention", "@opencode-ai/schema/prompt", PromptMention),
typeReference("FileAttachment", "@opencode-ai/schema/prompt", FileAttachment),
typeReference("AgentAttachment", "@opencode-ai/schema/prompt", AgentAttachment),
typeReference("AbsolutePath", "@opencode-ai/schema/schema", AbsolutePath),
typeReference("PositiveInt", "@opencode-ai/schema/schema", PositiveInt),
typeReference("RelativePath", "@opencode-ai/schema/schema", RelativePath),
]
await Effect.runPromise( await Effect.runPromise(
Effect.all( Effect.all(
@ -22,14 +97,40 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/promise/generated", import.meta.url)), fileURLToPath(new URL("../src/promise/generated", import.meta.url)),
), ),
write( write(
emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }), emitEffectImported(effectContract, {
module: "../../contract",
api: "ClientApi",
shapeModule: "../api/api.js",
}),
fileURLToPath(new URL("../src/effect/generated", import.meta.url)), fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
), ),
write( write(
emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }), emitEffectShape(effectContract, {
typeReferences: effectTypeReferences,
outputTypes: {
"event.subscribe": {
name: "OpenCodeEvent",
import: 'import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"',
},
},
}),
fileURLToPath(new URL("../src/effect/api", import.meta.url)), fileURLToPath(new URL("../src/effect/api", import.meta.url)),
), ),
], ],
{ concurrency: 3, discard: true }, { concurrency: 3, discard: true },
).pipe(Effect.provide(NodeFileSystem.layer)), ).pipe(Effect.provide(NodeFileSystem.layer)),
) )
function namespaceTypes(namespace: string, module: string, values: object) {
return Object.entries(values).flatMap(([name, schema]) =>
Schema.isSchema(schema) ? [typeReference(`${namespace}.${name}`, module, schema)] : [],
)
}
function typeReference(name: string, module: string, schema: Schema.Top) {
return {
schema,
name,
import: `import type { ${name.split(".")[0]} } from ${JSON.stringify(module)}`,
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,8 @@ import type {
LocationGetOutput, LocationGetOutput,
AgentListInput, AgentListInput,
AgentListOutput, AgentListOutput,
AgentGetInput,
AgentGetOutput,
PluginListInput, PluginListInput,
PluginListOutput, PluginListOutput,
SessionListInput, SessionListInput,
@ -403,6 +405,18 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
get: (input: AgentGetInput, requestOptions?: RequestOptions) =>
request<AgentGetOutput>(
{
method: "GET",
path: `/api/agent/${encodeURIComponent(input.agentID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [404, 401, 400],
empty: false,
},
requestOptions,
),
}, },
plugin: { plugin: {
list: (input?: PluginListInput, requestOptions?: RequestOptions) => list: (input?: PluginListInput, requestOptions?: RequestOptions) =>

View file

@ -6,11 +6,11 @@ export type ServiceStopResponse = { accepted: boolean }
export type ModelRef = { id: string; providerID: string; variant?: string } export type ModelRef = { id: string; providerID: string; variant?: string }
export type ProviderSettings = { [x: string]: JsonValue } export type ProviderSettings = { [x: string]: any }
export type AgentColor = string export type AgentColor = string
export type PermissionV2Effect = "allow" | "deny" | "ask" export type PermissionEffect = "allow" | "deny" | "ask"
export type PluginInfo = { id: string } export type PluginInfo = { id: string }
@ -175,24 +175,24 @@ export type ModelCapabilities = { tools: boolean; input: Array<string>; output:
export type ModelVariant = { export type ModelVariant = {
id: string id: string
settings?: { [x: string]: JsonValue } settings?: { [x: string]: any }
headers?: { [x: string]: string } headers?: { [x: string]: string }
body?: { [x: string]: JsonValue } body?: { [x: string]: any }
} }
export type MoneyUSDPerMillionTokens = number export type MoneyUSDPerMillionTokens = number
export type GenerateTextResponse = { data: { text: string } } export type GenerateTextResponse = { data: { text: string } }
export type ProviderV2Info = { export type ProviderInfo = {
id: string id: string
integrationID?: string integrationID?: string
name: string name: string
disabled?: boolean disabled?: boolean
package: string package: string
settings?: { [x: string]: JsonValue } settings?: { [x: string]: any }
headers?: { [x: string]: string } headers?: { [x: string]: string }
body?: { [x: string]: JsonValue } body?: { [x: string]: any }
} }
export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string } export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string }
@ -299,7 +299,7 @@ export type FormExternalField = { key: string; type: "external"; url: string; ti
export type FormValue = string | number | boolean | Array<string> export type FormValue = string | number | boolean | Array<string>
export type PermissionV2Source = { type: "tool"; messageID: string; callID: string } export type PermissionSource = { type: "tool"; messageID: string; callID: string }
export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string }
@ -323,9 +323,9 @@ export type FileDiffLegacyInfo = {
status?: "added" | "deleted" | "modified" status?: "added" | "deleted" | "modified"
} }
export type PermissionAction = "allow" | "deny" | "ask" export type PermissionV1Action = "allow" | "deny" | "ask"
export type JSONSchema = { [x: string]: any } export type SessionV1JSONSchema = { [x: string]: any }
export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } } export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } }
@ -356,7 +356,7 @@ export type APIError = {
} }
} }
export type TextPart = { export type SessionV1TextPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -368,7 +368,7 @@ export type TextPart = {
metadata?: { [x: string]: any } | undefined metadata?: { [x: string]: any } | undefined
} }
export type SubtaskPart = { export type SessionV1SubtaskPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -380,7 +380,7 @@ export type SubtaskPart = {
command?: string | undefined command?: string | undefined
} }
export type ReasoningPart = { export type SessionV1ReasoningPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -390,13 +390,13 @@ export type ReasoningPart = {
time: { start: number; end?: number | undefined } time: { start: number; end?: number | undefined }
} }
export type FilePartSourceText = { value: string; start: number; end: number } export type SessionV1FilePartSourceText = { value: string; start: number; end: number }
export type Range = { start: { line: number; character: number }; end: { line: number; character: number } } export type SessionV1Range = { start: { line: number; character: number }; end: { line: number; character: number } }
export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } export type SessionV1ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string }
export type ToolStateRunning = { export type SessionV1ToolStateRunning = {
status: "running" status: "running"
input: { [x: string]: any } input: { [x: string]: any }
title?: string | undefined title?: string | undefined
@ -404,7 +404,7 @@ export type ToolStateRunning = {
time: { start: number } time: { start: number }
} }
export type ToolStateError = { export type SessionV1ToolStateError = {
status: "error" status: "error"
input: { [x: string]: any } input: { [x: string]: any }
error: string error: string
@ -412,7 +412,7 @@ export type ToolStateError = {
time: { start: number; end: number } time: { start: number; end: number }
} }
export type StepStartPart = { export type SessionV1StepStartPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -420,7 +420,7 @@ export type StepStartPart = {
snapshot?: string | undefined snapshot?: string | undefined
} }
export type StepFinishPart = { export type SessionV1StepFinishPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -437,9 +437,15 @@ export type StepFinishPart = {
} }
} }
export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string } export type SessionV1SnapshotPart = {
id: string
sessionID: string
messageID: string
type: "snapshot"
snapshot: string
}
export type PatchPart = { export type SessionV1PatchPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -448,7 +454,7 @@ export type PatchPart = {
files: Array<string> files: Array<string>
} }
export type AgentPart = { export type SessionV1AgentPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -457,7 +463,7 @@ export type AgentPart = {
source?: { value: string; start: number; end: number } | undefined source?: { value: string; start: number; end: number } | undefined
} }
export type CompactionPart = { export type SessionV1CompactionPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -467,7 +473,7 @@ export type CompactionPart = {
tail_start_id?: string | undefined tail_start_id?: string | undefined
} }
export type PermissionV2Reply = "once" | "always" | "reject" export type PermissionReply = "once" | "always" | "reject"
export type Pty = { export type Pty = {
id: string id: string
@ -480,11 +486,11 @@ export type Pty = {
exitCode?: number exitCode?: number
} }
export type QuestionV2Option = { label: string; description: string } export type QuestionOption = { label: string; description: string }
export type QuestionV2Tool = { messageID: string; callID: string } export type QuestionTool = { messageID: string; callID: string }
export type QuestionV2Answer = Array<string> export type QuestionAnswer = Array<string>
export type FormMetadata1 = { [x: string]: any } export type FormMetadata1 = { [x: string]: any }
@ -501,12 +507,6 @@ export type SessionStatus =
} }
| { type: "busy" } | { type: "busy" }
export type QuestionOption = { label: string; description: string }
export type QuestionTool = { messageID: string; callID: string }
export type QuestionAnswer = Array<string>
export type ShellInfo1 = { export type ShellInfo1 = {
id: string id: string
status: "running" | "exited" | "timeout" | "killed" status: "running" | "exited" | "timeout" | "killed"
@ -564,10 +564,10 @@ export type CommandInfo = {
export type ProviderRequest = { export type ProviderRequest = {
settings: ProviderSettings settings: ProviderSettings
headers: { [x: string]: string } headers: { [x: string]: string }
body: { [x: string]: JsonValue } body: { [x: string]: any }
} }
export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect } export type PermissionRule = { action: string; resource: string; effect: PermissionEffect }
export type SessionAgentSelected = { export type SessionAgentSelected = {
id: string id: string
@ -1044,11 +1044,11 @@ export type ShellDeleted = {
data: { id: string } data: { id: string }
} }
export type QuestionV2Rejected = { export type QuestionRejected = {
id: string id: string
created: number created: number
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
type: "question.v2.rejected" type: "question.rejected"
location?: LocationRef location?: LocationRef
data: { sessionID: string; requestID: string } data: { sessionID: string; requestID: string }
} }
@ -1186,41 +1186,6 @@ export type McpResourcesChanged = {
data: { server: string } data: { server: string }
} }
export type PermissionAsked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "permission.asked"
location?: LocationRef
data: {
id: string
sessionID: string
permission: string
patterns: Array<string>
metadata: { [x: string]: any }
always: Array<string>
tool?: { messageID: string; callID: string } | undefined
}
}
export type PermissionReplied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "permission.replied"
location?: LocationRef
data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" }
}
export type QuestionRejected = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.rejected"
location?: LocationRef
data: { sessionID: string; requestID: string }
}
export type V2EventServerConnected = { export type V2EventServerConnected = {
id: string id: string
metadata?: { [x: string]: any } | undefined metadata?: { [x: string]: any } | undefined
@ -1544,21 +1509,21 @@ export type FormMultiselectField = {
export type FormAnswer = { [x: string]: FormValue } export type FormAnswer = { [x: string]: FormValue }
export type PermissionV2Request = { export type PermissionRequest = {
id: string id: string
sessionID: string sessionID: string
action: string action: string
resources: Array<string> resources: Array<string>
save?: Array<string> save?: Array<string>
metadata?: { [x: string]: JsonValue } metadata?: { [x: string]: JsonValue }
source?: PermissionV2Source source?: PermissionSource
} }
export type PermissionV2Asked = { export type PermissionAsked = {
id: string id: string
created: number created: number
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
type: "permission.v2.asked" type: "permission.asked"
location?: LocationRef location?: LocationRef
data: { data: {
id: string id: string
@ -1567,17 +1532,17 @@ export type PermissionV2Asked = {
resources: Array<string> resources: Array<string>
save?: Array<string> save?: Array<string>
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
source?: PermissionV2Source source?: PermissionSource
} }
} }
export type PermissionRule = { permission: string; pattern: string; action: PermissionAction } export type PermissionV1Rule = { permission: string; pattern: string; action: PermissionV1Action }
export type OutputFormat = export type SessionV1OutputFormat =
| { type: "text" } | { type: "text" }
| { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined } | { type: "json_schema"; schema: SessionV1JSONSchema; retryCount?: number | undefined | undefined }
export type AssistantMessage = { export type SessionV1AssistantMessage = {
id: string id: string
sessionID: string sessionID: string
role: "assistant" role: "assistant"
@ -1612,7 +1577,7 @@ export type AssistantMessage = {
finish?: string | undefined finish?: string | undefined
} }
export type RetryPart = { export type SessionV1RetryPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -1643,26 +1608,31 @@ export type SessionError = {
} }
} }
export type FileSource = { text: FilePartSourceText; type: "file"; path: string } export type SessionV1FileSource = { text: SessionV1FilePartSourceText; type: "file"; path: string }
export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } export type SessionV1ResourceSource = {
text: SessionV1FilePartSourceText
type: "resource"
clientName: string
uri: string
}
export type SymbolSource = { export type SessionV1SymbolSource = {
text: FilePartSourceText text: SessionV1FilePartSourceText
type: "symbol" type: "symbol"
path: string path: string
range: Range range: SessionV1Range
name: string name: string
kind: number kind: number
} }
export type PermissionV2Replied = { export type PermissionReplied = {
id: string id: string
created: number created: number
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
type: "permission.v2.replied" type: "permission.replied"
location?: LocationRef location?: LocationRef
data: { sessionID: string; requestID: string; reply: PermissionV2Reply } data: { sessionID: string; requestID: string; reply: PermissionReply }
} }
export type PtyCreated = { export type PtyCreated = {
@ -1683,21 +1653,21 @@ export type PtyUpdated = {
data: { info: Pty } data: { info: Pty }
} }
export type QuestionV2Info = { export type QuestionInfo = {
question: string question: string
header: string header: string
options: Array<QuestionV2Option> options: Array<QuestionOption>
multiple?: boolean multiple?: boolean
custom?: boolean custom?: boolean
} }
export type QuestionV2Replied = { export type QuestionReplied = {
id: string id: string
created: number created: number
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
type: "question.v2.replied" type: "question.replied"
location?: LocationRef location?: LocationRef
data: { sessionID: string; requestID: string; answers: Array<QuestionV2Answer> } data: { sessionID: string; requestID: string; answers: Array<QuestionAnswer> }
} }
export type FormStringField1 = { export type FormStringField1 = {
@ -1774,26 +1744,9 @@ export type SessionStatus2 = {
data: { sessionID: string; status: SessionStatus } data: { sessionID: string; status: SessionStatus }
} }
export type QuestionInfo = {
question: string
header: string
options: Array<QuestionOption>
multiple?: boolean | undefined
custom?: boolean | undefined
}
export type QuestionReplied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.replied"
location?: LocationRef
data: { sessionID: string; requestID: string; answers: Array<QuestionAnswer> }
}
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
export type PermissionV2Ruleset = Array<PermissionV2Rule> export type PermissionRuleset = Array<PermissionRule>
export type SessionInfo = { export type SessionInfo = {
id: string id: string
@ -1910,9 +1863,9 @@ export type ModelInfo = {
name: string name: string
compatibility?: ModelCompatibility compatibility?: ModelCompatibility
package?: string package?: string
settings?: { [x: string]: JsonValue } settings?: { [x: string]: any }
headers?: { [x: string]: string } headers?: { [x: string]: string }
body?: { [x: string]: JsonValue } body?: { [x: string]: any }
capabilities: ModelCapabilities capabilities: ModelCapabilities
variants: Array<ModelVariant> variants: Array<ModelVariant>
time: { released: number } time: { released: number }
@ -1948,14 +1901,14 @@ export type FormReplied = {
data: { id: string; sessionID: string; answer: FormAnswer } data: { id: string; sessionID: string; answer: FormAnswer }
} }
export type PermissionRuleset = Array<PermissionRule> export type PermissionV1Ruleset = Array<PermissionV1Rule>
export type UserMessage = { export type SessionV1UserMessage = {
id: string id: string
sessionID: string sessionID: string
role: "user" role: "user"
time: { created: number } time: { created: number }
format?: OutputFormat | undefined format?: SessionV1OutputFormat | undefined
summary?: { title?: string | undefined; body?: string | undefined; diffs: Array<FileDiffLegacyInfo> } | undefined summary?: { title?: string | undefined; body?: string | undefined; diffs: Array<FileDiffLegacyInfo> } | undefined
agent: string agent: string
model: { providerID: string; modelID: string; variant?: string | undefined } model: { providerID: string; modelID: string; variant?: string | undefined }
@ -1963,23 +1916,18 @@ export type UserMessage = {
tools?: { [x: string]: boolean } | undefined tools?: { [x: string]: boolean } | undefined
} }
export type FilePartSource = FileSource | SymbolSource | ResourceSource export type SessionV1FilePartSource = SessionV1FileSource | SessionV1SymbolSource | SessionV1ResourceSource
export type QuestionV2Asked = { export type QuestionAsked = {
id: string id: string
created: number created: number
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
type: "question.v2.asked" type: "question.asked"
location?: LocationRef location?: LocationRef
data: { id: string; sessionID: string; questions: Array<QuestionV2Info>; tool?: QuestionV2Tool } data: { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
} }
export type QuestionV2Request = { export type QuestionRequest = { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
id: string
sessionID: string
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
}
export type FormField1 = export type FormField1 =
| FormStringField1 | FormStringField1
@ -1989,15 +1937,6 @@ export type FormField1 =
| FormMultiselectField1 | FormMultiselectField1
| FormExternalField | FormExternalField
export type QuestionAsked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.asked"
location?: LocationRef
data: { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool | undefined }
}
export type ReferenceInfo = { export type ReferenceInfo = {
name: string name: string
path: string path: string
@ -2017,7 +1956,7 @@ export type AgentInfo = {
hidden: boolean hidden: boolean
color?: AgentColor color?: AgentColor
steps?: number steps?: number
permissions: PermissionV2Ruleset permissions: PermissionRuleset
} }
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } } export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
@ -2075,13 +2014,13 @@ export type SessionV1Info = {
version: string version: string
metadata?: { [x: string]: any } metadata?: { [x: string]: any }
time: { created: number; updated: number; compacting?: number; archived?: number } time: { created: number; updated: number; compacting?: number; archived?: number }
permission?: PermissionRuleset permission?: PermissionV1Ruleset
revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string } revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string }
} }
export type Message = UserMessage | AssistantMessage export type SessionV1Message = SessionV1UserMessage | SessionV1AssistantMessage
export type FilePart = { export type SessionV1FilePart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
@ -2089,7 +2028,7 @@ export type FilePart = {
mime: string mime: string
filename?: string | undefined filename?: string | undefined
url: string url: string
source?: FilePartSource | undefined source?: SessionV1FilePartSource | undefined
} }
export type FormFields1 = [FormField1, ...Array<FormField1>] export type FormFields1 = [FormField1, ...Array<FormField1>]
@ -2160,17 +2099,17 @@ export type MessageUpdated = {
type: "message.updated" type: "message.updated"
durable: { aggregateID: string; seq: number; version: 1 } durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef location?: LocationRef
data: { sessionID: string; info: Message } data: { sessionID: string; info: SessionV1Message }
} }
export type ToolStateCompleted = { export type SessionV1ToolStateCompleted = {
status: "completed" status: "completed"
input: { [x: string]: any } input: { [x: string]: any }
output: string output: string
title: string title: string
metadata: { [x: string]: any } metadata: { [x: string]: any }
time: { start: number; end: number; compacted?: number | undefined } time: { start: number; end: number; compacted?: number | undefined }
attachments?: Array<FilePart> | undefined attachments?: Array<SessionV1FilePart> | undefined
} }
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 } export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 }
@ -2196,7 +2135,11 @@ export type SessionMessageInfo =
| SessionMessageAssistant | SessionMessageAssistant
| SessionMessageCompaction | SessionMessageCompaction
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError export type SessionV1ToolState =
| SessionV1ToolStatePending
| SessionV1ToolStateRunning
| SessionV1ToolStateCompleted
| SessionV1ToolStateError
export type FormCreated = { export type FormCreated = {
id: string id: string
@ -2252,32 +2195,32 @@ export type SessionMessagesResponse = {
cursor: { previous?: string | null; next?: string | null } cursor: { previous?: string | null; next?: string | null }
} }
export type ToolPart = { export type SessionV1ToolPart = {
id: string id: string
sessionID: string sessionID: string
messageID: string messageID: string
type: "tool" type: "tool"
callID: string callID: string
tool: string tool: string
state: ToolState state: SessionV1ToolState
metadata?: { [x: string]: any } | undefined metadata?: { [x: string]: any } | undefined
} }
export type SessionLogItem = SessionEventDurable | EventLogSynced export type SessionLogItem = SessionEventDurable | EventLogSynced
export type Part = export type SessionV1Part =
| TextPart | SessionV1TextPart
| SubtaskPart | SessionV1SubtaskPart
| ReasoningPart | SessionV1ReasoningPart
| FilePart | SessionV1FilePart
| ToolPart | SessionV1ToolPart
| StepStartPart | SessionV1StepStartPart
| StepFinishPart | SessionV1StepFinishPart
| SnapshotPart | SessionV1SnapshotPart
| PatchPart | SessionV1PatchPart
| AgentPart | SessionV1AgentPart
| RetryPart | SessionV1RetryPart
| CompactionPart | SessionV1CompactionPart
export type MessagePartUpdated = { export type MessagePartUpdated = {
id: string id: string
@ -2286,7 +2229,7 @@ export type MessagePartUpdated = {
type: "message.part.updated" type: "message.part.updated"
durable: { aggregateID: string; seq: number; version: 1 } durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef location?: LocationRef
data: { sessionID: string; part: Part; time: number } data: { sessionID: string; part: SessionV1Part; time: number }
} }
export type V2Event = export type V2Event =
@ -2347,8 +2290,8 @@ export type V2Event =
| SessionRevertCommitted | SessionRevertCommitted
| FilesystemChanged | FilesystemChanged
| ReferenceUpdated | ReferenceUpdated
| PermissionV2Asked | PermissionAsked
| PermissionV2Replied | PermissionReplied
| PluginAdded | PluginAdded
| PluginUpdated | PluginUpdated
| ProjectDirectoriesUpdated | ProjectDirectoriesUpdated
@ -2362,9 +2305,9 @@ export type V2Event =
| ShellCreated | ShellCreated
| ShellExited | ShellExited
| ShellDeleted | ShellDeleted
| QuestionV2Asked | QuestionAsked
| QuestionV2Replied | QuestionReplied
| QuestionV2Rejected | QuestionRejected
| FormCreated | FormCreated
| FormReplied | FormReplied
| FormCancelled | FormCancelled
@ -2380,11 +2323,6 @@ export type V2Event =
| VcsBranchUpdated | VcsBranchUpdated
| McpStatusChanged | McpStatusChanged
| McpResourcesChanged | McpResourcesChanged
| PermissionAsked
| PermissionReplied
| QuestionAsked
| QuestionReplied
| QuestionRejected
| SessionError | SessionError
| V2EventServerConnected | V2EventServerConnected
@ -2401,6 +2339,14 @@ export type InvalidRequestError = {
export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => export const isInvalidRequestError = (value: unknown): value is InvalidRequestError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError"
export type AgentNotFoundError = {
readonly _tag: "AgentNotFoundError"
readonly agentID: string
readonly message: string
}
export const isAgentNotFoundError = (value: unknown): value is AgentNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "AgentNotFoundError"
export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string }
export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => export const isInvalidCursorError = (value: unknown): value is InvalidCursorError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError"
@ -2584,6 +2530,18 @@ export type AgentListOutput = {
data: Array<AgentInfo> data: Array<AgentInfo>
} }
export type AgentGetInput = {
readonly agentID: { readonly agentID: string }["agentID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type AgentGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: AgentInfo
}
export type PluginListInput = { export type PluginListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@ -3312,7 +3270,7 @@ export type ProviderListInput = {
export type ProviderListOutput = { export type ProviderListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ProviderV2Info> data: Array<ProviderInfo>
} }
export type ProviderGetInput = { export type ProviderGetInput = {
@ -3324,7 +3282,7 @@ export type ProviderGetInput = {
export type ProviderGetOutput = { export type ProviderGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ProviderV2Info data: ProviderInfo
} }
export type IntegrationListInput = { export type IntegrationListInput = {
@ -4476,7 +4434,7 @@ export type PermissionRequestListInput = {
export type PermissionRequestListOutput = { export type PermissionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<PermissionV2Request> data: Array<PermissionRequest>
} }
export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] }
@ -4554,18 +4512,18 @@ export type PermissionCreateInput = {
}["agent"] }["agent"]
} }
export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"] export type PermissionCreateOutput = { data: { id: string; effect: PermissionEffect } }["data"]
export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type PermissionListOutput = { data: Array<PermissionV2Request> }["data"] export type PermissionListOutput = { data: Array<PermissionRequest> }["data"]
export type PermissionGetInput = { export type PermissionGetInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"]
} }
export type PermissionGetOutput = { data: PermissionV2Request }["data"] export type PermissionGetOutput = { data: PermissionRequest }["data"]
export type PermissionReplyInput = { export type PermissionReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
@ -4864,12 +4822,12 @@ export type QuestionRequestListInput = {
export type QuestionRequestListOutput = { export type QuestionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<QuestionV2Request> data: Array<QuestionRequest>
} }
export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type QuestionListOutput = { data: Array<QuestionV2Request> }["data"] export type QuestionListOutput = { data: Array<QuestionRequest> }["data"]
export type QuestionReplyInput = { export type QuestionReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]

View file

@ -1,5 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect" import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
import type { Session } from "@opencode-ai/schema/session"
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>> type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make> type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
@ -9,6 +10,10 @@ declare const promiseClient: PromiseClient
const effectApi: EffectApi<unknown> = effectClient const effectApi: EffectApi<unknown> = effectClient
const effectSession: Effect.Effect<Session.Info, unknown> = effectClient.session.get({
sessionID: "ses_test" as Session.ID,
})
declare const sessionID: Parameters<typeof effectApi.session.instructions.entry.list>[0]["sessionID"] declare const sessionID: Parameters<typeof effectApi.session.instructions.entry.list>[0]["sessionID"]
const effectList: Effect.Effect< const effectList: Effect.Effect<
@ -37,4 +42,4 @@ const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.re
key: "review-notes", key: "review-notes",
}) })
void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]

View file

@ -14,6 +14,14 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
expect(Client.Session).toBe(Session) expect(Client.Session).toBe(Session)
}) })
test("generated Effect API names canonical and composed outputs", async () => {
const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text()
expect(source).toContain("export type Endpoint5_3Output = Session.Info")
expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent")
expect(source).not.toContain("HttpApiClient.ForApi")
})
test("shared DTO schemas construct and decode plain objects", () => { test("shared DTO schemas construct and decode plain objects", () => {
const made = Prompt.make({ text: "hello" }) const made = Prompt.make({ text: "hello" })
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" }) const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })

View file

@ -99,6 +99,7 @@
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@standard-schema/spec": "catalog:",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4", "@silvia-odwyer/photon-node": "0.3.4",
"@openrouter/ai-sdk-provider": "2.9.0", "@openrouter/ai-sdk-provider": "2.9.0",

View file

@ -1,4 +1,4 @@
export * as AccountV2 from "./account" export * as Account from "./account"
import { Schema } from "effect" import { Schema } from "effect"
import type { HttpClientError } from "effect/unstable/http" import type { HttpClientError } from "effect/unstable/http"

View file

@ -1,14 +1,14 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
import { AccountV2 } from "../account" import { Account } from "../account"
import { Timestamps } from "../database/schema.sql" import { Timestamps } from "../database/schema.sql"
export const AccountTable = sqliteTable("account", { export const AccountTable = sqliteTable("account", {
id: text().$type<AccountV2.ID>().primaryKey(), id: text().$type<Account.ID>().primaryKey(),
email: text().notNull(), email: text().notNull(),
url: text().notNull(), url: text().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(), access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(), refresh_token: text().$type<Account.RefreshToken>().notNull(),
token_expiry: integer(), token_expiry: integer(),
...Timestamps, ...Timestamps,
}) })
@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", {
export const AccountStateTable = sqliteTable("account_state", { export const AccountStateTable = sqliteTable("account_state", {
id: integer().primaryKey(), id: integer().primaryKey(),
active_account_id: text() active_account_id: text()
.$type<AccountV2.ID>() .$type<Account.ID>()
.references(() => AccountTable.id, { onDelete: "set null" }), .references(() => AccountTable.id, { onDelete: "set null" }),
active_org_id: text().$type<AccountV2.OrgID>(), active_org_id: text().$type<Account.OrgID>(),
}) })
// LEGACY // LEGACY
@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable(
{ {
email: text().notNull(), email: text().notNull(),
url: text().notNull(), url: text().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(), access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(), refresh_token: text().$type<Account.RefreshToken>().notNull(),
token_expiry: integer(), token_expiry: integer(),
active: integer({ mode: "boolean" }) active: integer({ mode: "boolean" })
.notNull() .notNull()

View file

@ -1,9 +1,9 @@
export * as AgentV2 from "./agent" export * as Agent from "./agent"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Types } from "effect" import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { State } from "./state" import { State } from "./state"
export const ID = Agent.ID export const ID = Agent.ID
@ -17,7 +17,7 @@ export const Color = Agent.Color
export const Info = Agent.Info export const Info = Agent.Info
export type Info = Agent.Info export type Info = Agent.Info
export const Event = Agent.Event export { Event } from "@opencode-ai/schema/agent"
export interface Selection { export interface Selection {
readonly id: ID readonly id: ID
@ -45,12 +45,12 @@ export interface Interface extends State.Transformable<Draft> {
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
name: "agent", name: "agent",
initial: () => ({ agents: new Map() }), initial: () => ({ agents: new Map() }),
@ -70,7 +70,7 @@ const layer = Layer.effect(
draft.agents.delete(id) draft.agents.delete(id)
}, },
}), }),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), finalize: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid),
}) })
const selectable = (agent: Info | undefined) => const selectable = (agent: Info | undefined) =>
agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
@ -89,17 +89,17 @@ const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
reload: state.reload, reload: state.reload,
get: Effect.fn("AgentV2.get")(function* (id) { get: Effect.fn("Agent.get")(function* (id) {
return state.get().agents.get(id) return state.get().agents.get(id)
}), }),
default: Effect.fn("AgentV2.default")(function* () { default: Effect.fn("Agent.default")(function* () {
return selectedDefault() return selectedDefault()
}), }),
resolve: Effect.fn("AgentV2.resolve")(function* (id) { resolve: Effect.fn("Agent.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id)) if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault() return selectedDefault()
}), }),
select: Effect.fn("AgentV2.select")(function* (id) { select: Effect.fn("Agent.select")(function* (id) {
if (id !== undefined) { if (id !== undefined) {
const selected = ID.make(id) const selected = ID.make(id)
return { id: selected, info: state.get().agents.get(selected) } return { id: selected, info: state.get().agents.get(selected) }
@ -107,11 +107,11 @@ const layer = Layer.effect(
const info = selectedDefault() const info = selectedDefault()
return { id: info?.id ?? defaultID, info } return { id: info?.id ?? defaultID, info }
}), }),
list: Effect.fn("AgentV2.list")(function* () { list: Effect.fn("Agent.list")(function* () {
return Array.fromIterable(state.get().agents.values()) return Array.fromIterable(state.get().agents.values())
}), }),
}) })
}), }),
) )
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })

View file

@ -32,8 +32,8 @@ import {
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route" import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { ProviderShared } from "@opencode-ai/ai/protocols/shared" import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect" import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model" import type { ID, Info } from "./model"
import { ProviderV2 } from "./provider" import { Provider } from "./provider"
import { State } from "./state" import { State } from "./state"
type SDK = any type SDK = any
@ -42,14 +42,14 @@ type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }> type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
export interface SDKEvent { export interface SDKEvent {
readonly model: ModelV2.Info readonly model: Info
readonly package: string readonly package: string
readonly options: Record<string, any> readonly options: Record<string, any>
sdk?: SDK sdk?: SDK
} }
export interface LanguageEvent { export interface LanguageEvent {
readonly model: ModelV2.Info readonly model: Info
readonly sdk: SDK readonly sdk: SDK
readonly options: Record<string, any> readonly options: Record<string, any>
language?: LanguageModelV3 language?: LanguageModelV3
@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
}) })
} }
function prepareOptions(model: ModelV2.Info, pkg: string) { function prepareOptions(model: Info, pkg: string) {
const projected = mapBodyToProviderOptions(model, pkg) const projected = mapBodyToProviderOptions(model, pkg)
const options: Record<string, any> = { const options: Record<string, any> = {
name: model.providerID, name: model.providerID,
@ -146,7 +146,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
if (typeof opts.body === "string" && model.body !== undefined) { if (typeof opts.body === "string" && model.body !== undefined) {
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body)) const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) { if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body)) opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body))
} }
} }
@ -162,11 +162,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
} }
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", { export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID, providerID: Provider.ID,
cause: Schema.Defect(), cause: Schema.Defect(),
}) {} }) {}
function initError(providerID: ProviderV2.ID) { function initError(providerID: Provider.ID) {
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) }))) return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
} }
@ -181,11 +181,11 @@ export interface Interface {
} }
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent> readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent> readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError> readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError> readonly model: (model: Info) => Effect.Effect<Model, InitError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {} export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
export const locationLayer = Layer.effect( export const locationLayer = Layer.effect(
Service, Service,
@ -260,13 +260,13 @@ export const locationLayer = Layer.effect(
}) })
const existing = languages.get(key) const existing = languages.get(key)
if (existing) return existing if (existing) return existing
if (!ProviderV2.isAISDK(model.package)) if (!Provider.isAISDK(model.package))
return yield* new InitError({ return yield* new InitError({
providerID: model.providerID, providerID: model.providerID,
cause: new Error(`Unsupported package ${model.package}`), cause: new Error(`Unsupported package ${model.package}`),
}) })
const packageName = ProviderV2.packageName(model.package) const packageName = Provider.packageName(model.package)
const options = prepareOptions(model, packageName) const options = prepareOptions(model, packageName)
const sdkKey = cacheKey({ const sdkKey = cacheKey({
providerID: model.providerID, providerID: model.providerID,
@ -301,8 +301,8 @@ export const locationLayer = Layer.effect(
export const defaultLayer = locationLayer export const defaultLayer = locationLayer
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { function modelFromLanguage(info: Info, language: LanguageModelV3) {
const packageName = ProviderV2.packageName(info.package!) const packageName = Provider.packageName(info.package!)
const projected = mapBodyToProviderOptions(info, packageName) const projected = mapBodyToProviderOptions(info, packageName)
const optionKey = providerOptionKey(packageName, info.providerID) const optionKey = providerOptionKey(packageName, info.providerID)
const providerOptions = (() => { const providerOptions = (() => {
@ -352,7 +352,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
}) })
} }
function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) { function gatewayProviderOptions(modelID: ID, settings: Readonly<Record<string, unknown>>) {
const gateway = const gateway =
typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway) typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
? Object.fromEntries(Object.entries(settings.gateway)) ? Object.fromEntries(Object.entries(settings.gateway))
@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<s
return { gateway: model } return { gateway: model }
} }
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) { function providerOptionKey(packageName: string | undefined, providerID: Provider.ID) {
if (packageName === "@ai-sdk/google") return "google" if (packageName === "@ai-sdk/google") return "google"
if (packageName === "@ai-sdk/google-vertex") return "vertex" if (packageName === "@ai-sdk/google-vertex") return "vertex"
if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic" if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
@ -395,18 +395,18 @@ function requestSettings(settings: Readonly<Record<string, unknown>> | undefined
return Object.keys(result).length === 0 ? undefined : result return Object.keys(result).length === 0 ? undefined : result
} }
function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) { function mapBodyToProviderOptions(model: Info, packageName: string) {
const settings = requestSettings(model.settings) const settings = requestSettings(model.settings)
const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning) const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
const forceReasoning = const forceReasoning =
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) && ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
(pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined) (pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings
if (!pro) return { settings: normalized, body: model.body } if (!pro) return { settings: normalized, body: model.body }
const body = { ...model.body } const body = { ...model.body }
delete body.reasoning delete body.reasoning
return { return {
settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }), settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }),
body: Object.keys(body).length === 0 ? undefined : body, body: Object.keys(body).length === 0 ? undefined : body,
} }
} }

View file

@ -1,8 +1,7 @@
export * as EventV2 from "./event" export * as Bus from "./bus"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event" import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log" import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm"
import { Database } from "./database/database" import { Database } from "./database/database"
@ -12,18 +11,10 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest" import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Event.ID export type Subscriber<D extends Event.Definition = Event.Definition> = (event: Event.Payload<D>) => Effect.Effect<void>
export type ID = import("@opencode-ai/schema/event").ID
export const Seq = Event.Seq
export type Seq = import("@opencode-ai/schema/event").Seq
export const Version = Event.Version
export type Version = import("@opencode-ai/schema/event").Version
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void> export type Unsubscribe = Effect.Effect<void>
export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( export const latestSequence = Effect.fn("Bus.latestSequence")(function* (
db: Database.Interface["db"], db: Database.Interface["db"],
aggregateID: string, aggregateID: string,
) { ) {
@ -36,7 +27,7 @@ export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
return row?.seq ?? -1 return row?.seq ?? -1
}) })
export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* ( export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* (
db: Database.Interface["db"], db: Database.Interface["db"],
aggregateID: string, aggregateID: string,
seq: number, seq: number,
@ -53,7 +44,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
}) })
export type SerializedEvent = { export type SerializedEvent = {
readonly id: ID readonly id: Event.ID
readonly type: string readonly type: string
readonly created?: DateTime.Utc readonly created?: DateTime.Utc
readonly seq: number readonly seq: number
@ -62,7 +53,7 @@ export type SerializedEvent = {
} }
export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()( export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()(
"EventV2.InvalidDurableEvent", "Bus.InvalidDurableEvent",
{ {
type: Schema.String, type: Schema.String,
message: Schema.String, message: Schema.String,
@ -71,11 +62,11 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
const envelope = (aggregateID: string, seq: number, version: number) => ({ const envelope = (aggregateID: string, seq: number, version: number) => ({
aggregateID, aggregateID,
seq: Seq.make(seq), seq: Event.Seq.make(seq),
version: Version.make(version), version: Event.Version.make(version),
}) })
const decodeSerializedEvent = (event: SerializedEvent): Payload => { const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => {
const definition = Durable.get(event.type) const definition = Durable.get(event.type)
if (!definition?.durable) { if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
@ -94,7 +85,7 @@ export const durable = Event.durable
export const ephemeral = Event.ephemeral export const ephemeral = Event.ephemeral
export interface PublishOptions { export interface PublishOptions {
readonly id?: ID readonly id?: Event.ID
readonly metadata?: Record<string, unknown> readonly metadata?: Record<string, unknown>
readonly location?: Location.Ref readonly location?: Location.Ref
/** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */ /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
@ -102,13 +93,13 @@ export interface PublishOptions {
} }
/** Marker/event union emitted by `log`. */ /** Marker/event union emitted by `log`. */
export type LogItem = Payload | EventLog.Synced export type LogItem = Event.Payload | EventLog.Synced
export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced" export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced"
export type SubscribePayload<D extends readonly Definition[]> = D[number] extends infer Item export type SubscribePayload<D extends readonly Event.Definition[]> = D[number] extends infer Item
? Item extends Definition ? Item extends Event.Definition
? Payload<Item> ? Event.Payload<Item>
: never : never
: never : never
@ -117,19 +108,19 @@ export interface Subscribe {
* Volatile live channel: every event published from now on, nothing before or * Volatile live channel: every event published from now on, nothing before or
* across a disconnect. Consumers that need reliability combine it with `log`. * across a disconnect. Consumers that need reliability combine it with `log`.
*/ */
(): Stream.Stream<Payload> (): Stream.Stream<Event.Payload>
<D extends Definition>(definition: D): Stream.Stream<Payload<D>> <D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
<const D extends readonly [Definition, ...Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>> <const D extends readonly [Event.Definition, ...Event.Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
} }
const isDefinition = (input: Definition | readonly Definition[]): input is Definition => !Array.isArray(input) const isDefinition = (input: Event.Definition | readonly Event.Definition[]): input is Event.Definition => !Array.isArray(input)
export interface Interface { export interface Interface {
readonly publish: <D extends Definition>( readonly publish: <D extends Event.Definition>(
definition: D, definition: D,
data: Data<D>, data: Event.Data<D>,
options?: PublishOptions, options?: PublishOptions,
) => Effect.Effect<Payload<D>> ) => Effect.Effect<Event.Payload<D>>
readonly subscribe: Subscribe readonly subscribe: Subscribe
/** /**
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an * Durable, ordered per-aggregate log read. Forked aggregates may reserve an
@ -144,10 +135,10 @@ export interface Interface {
readonly follow?: boolean readonly follow?: boolean
}) => Stream.Stream<LogItem> }) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */ /** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>> readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Event.Seq>>
/** @deprecated Use `subscribe()` and consume the returned stream. */ /** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe> readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void> readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly replay: ( readonly replay: (
event: SerializedEvent, event: SerializedEvent,
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
@ -160,7 +151,8 @@ export interface Interface {
readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void> readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
export interface LayerOptions { export interface LayerOptions {
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void> readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
@ -173,20 +165,20 @@ export const layerWith = (options?: LayerOptions) =>
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const pubsub = { const pubsub = {
live: yield* PubSub.unbounded<Payload>(), live: yield* PubSub.unbounded<Event.Payload>(),
durable: new Map<string, Set<PubSub.PubSub<void>>>(), durable: new Map<string, Set<PubSub.PubSub<void>>>(),
typed: new Map<string, PubSub.PubSub<Payload>>(), typed: new Map<string, PubSub.PubSub<Event.Payload>>(),
} }
const projectors = new Map<string, Subscriber[]>() const projectors = new Map<string, Subscriber[]>()
const listeners = new Array<Subscriber>() const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service const { db } = yield* Database.Service
const logReadPageSize = options?.logReadPageSize ?? 512 const logReadPageSize = options?.logReadPageSize ?? 512
const getOrCreate = (definition: Definition) => const getOrCreate = (definition: Event.Definition) =>
Effect.gen(function* () { Effect.gen(function* () {
const existing = pubsub.typed.get(definition.type) const existing = pubsub.typed.get(definition.type)
if (existing) return existing if (existing) return existing
const created = yield* PubSub.unbounded<Payload>() const created = yield* PubSub.unbounded<Event.Payload>()
pubsub.typed.set(definition.type, created) pubsub.typed.set(definition.type, created)
return created return created
}) })
@ -204,8 +196,8 @@ export const layerWith = (options?: LayerOptions) =>
) )
function commitDurableEvent( function commitDurableEvent(
definition: Definition, definition: Event.Definition,
event: Payload, event: Event.Payload,
input?: { input?: {
readonly seq: number readonly seq: number
readonly aggregateID: string readonly aggregateID: string
@ -318,7 +310,7 @@ export const layerWith = (options?: LayerOptions) =>
const committed = { const committed = {
...event, ...event,
durable: { aggregateID, seq, version: durable.version }, durable: { aggregateID, seq, version: durable.version },
} as Payload } as Event.Payload
for (const projector of list) { for (const projector of list) {
yield* projector(committed) yield* projector(committed)
} }
@ -369,7 +361,7 @@ export const layerWith = (options?: LayerOptions) =>
}) })
} }
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) { function publishEvent<D extends Event.Definition>(definition: D, event: Event.Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () { return Effect.gen(function* () {
if (!definition?.durable && commit) if (!definition?.durable && commit)
return yield* Effect.die( return yield* Effect.die(
@ -379,22 +371,22 @@ export const layerWith = (options?: LayerOptions) =>
}), }),
) )
if (definition?.durable) { if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
if (committed) { if (committed) {
event = { event = {
...event, ...event,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version), durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
} }
yield* notify(event as Payload, true) yield* notify(event as Event.Payload, true)
return event return event
} }
} }
yield* notify(event as Payload, false) yield* notify(event as Event.Payload, false)
return event return event
}) })
} }
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) => const observe = (event: Event.Payload, observer: (event: Event.Payload) => Effect.Effect<void>) =>
Effect.suspend(() => observer(event)).pipe( Effect.suspend(() => observer(event)).pipe(
Effect.catchCauseIf( Effect.catchCauseIf(
(cause) => !Cause.hasInterrupts(cause), (cause) => !Cause.hasInterrupts(cause),
@ -402,7 +394,7 @@ export const layerWith = (options?: LayerOptions) =>
), ),
) )
function notify(event: Payload, isolateListeners: boolean) { function notify(event: Event.Payload, isolateListeners: boolean) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* Effect.forEach( yield* Effect.forEach(
listeners, listeners,
@ -415,7 +407,7 @@ export const layerWith = (options?: LayerOptions) =>
}) })
} }
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) { function publish<D extends Event.Definition>(definition: D, data: Event.Data<D>, options?: PublishOptions) {
return Effect.gen(function* () { return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location = const location =
@ -426,13 +418,13 @@ export const layerWith = (options?: LayerOptions) =>
return yield* publishEvent( return yield* publishEvent(
definition, definition,
{ {
id: options?.id ?? ID.create(), id: options?.id ?? Event.ID.create(),
created: yield* DateTime.now, created: yield* DateTime.now,
...(options?.metadata ? { metadata: options.metadata } : {}), ...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type, type: definition.type,
...(location ? { location } : {}), ...(location ? { location } : {}),
data, data,
} as Payload<D>, } as Event.Payload<D>,
options?.commit, options?.commit,
) )
}) })
@ -454,7 +446,7 @@ export const layerWith = (options?: LayerOptions) =>
created: event.created ?? DateTime.makeUnsafe(0), created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type, type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data), data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Payload } as Event.Payload
const committed = yield* commitDurableEvent(definition, payload, { const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq, seq: event.seq,
aggregateID: event.aggregateID, aggregateID: event.aggregateID,
@ -516,7 +508,7 @@ export const layerWith = (options?: LayerOptions) =>
.pipe(Effect.orDie) .pipe(Effect.orDie)
} }
const local = <A extends Payload>(stream: Stream.Stream<A>) => const local = <A extends Event.Payload>(stream: Stream.Stream<A>) =>
Stream.unwrap( Stream.unwrap(
Effect.serviceOption(Location.Service).pipe( Effect.serviceOption(Location.Service).pipe(
Effect.map((location) => Effect.map((location) =>
@ -536,12 +528,12 @@ export const layerWith = (options?: LayerOptions) =>
), ),
) )
function subscribe(): Stream.Stream<Payload> function subscribe(): Stream.Stream<Event.Payload>
function subscribe<D extends Definition>(definition: D): Stream.Stream<Payload<D>> function subscribe<D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
function subscribe<const D extends readonly [Definition, ...Definition[]]>( function subscribe<const D extends readonly [Event.Definition, ...Event.Definition[]]>(
definitions: D, definitions: D,
): Stream.Stream<SubscribePayload<D>> ): Stream.Stream<SubscribePayload<D>>
function subscribe(input?: Definition | readonly Definition[]): Stream.Stream<Payload> { function subscribe(input?: Event.Definition | readonly Event.Definition[]): Stream.Stream<Event.Payload> {
if (input === undefined) return streamLive() if (input === undefined) return streamLive()
if (isDefinition(input)) { if (isDefinition(input)) {
return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))) return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))))
@ -550,7 +542,7 @@ export const layerWith = (options?: LayerOptions) =>
return streamLive().pipe(Stream.filter((event) => types.has(event.type))) return streamLive().pipe(Stream.filter((event) => types.has(event.type)))
} }
const streamLive = (): Stream.Stream<Payload> => local(Stream.fromPubSub(pubsub.live)) const streamLive = (): Stream.Stream<Event.Payload> => local(Stream.fromPubSub(pubsub.live))
const readAfter = ( const readAfter = (
aggregateID: string, aggregateID: string,
@ -624,7 +616,7 @@ export const layerWith = (options?: LayerOptions) =>
Stream.unwrap( Stream.unwrap(
Effect.gen(function* () { Effect.gen(function* () {
let sequence = input.after ?? -1 let sequence = input.after ?? -1
const readThrough = (through: number): Stream.Stream<Payload> => const readThrough = (through: number): Stream.Stream<Event.Payload> =>
Stream.paginate(sequence, (cursor) => Stream.paginate(sequence, (cursor) =>
readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe( readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe(
Effect.tap((page) => Effect.tap((page) =>
@ -648,7 +640,7 @@ export const layerWith = (options?: LayerOptions) =>
const marker: EventLog.Synced = { const marker: EventLog.Synced = {
type: "log.synced", type: "log.synced",
aggregateID: input.aggregateID, aggregateID: input.aggregateID,
...(target >= 0 ? { seq: Seq.make(target) } : {}), ...(target >= 0 ? { seq: Event.Seq.make(target) } : {}),
} }
const replay: Stream.Stream<LogItem> = readThrough(target).pipe( const replay: Stream.Stream<LogItem> = readThrough(target).pipe(
Stream.map((event): LogItem => event), Stream.map((event): LogItem => event),
@ -665,7 +657,7 @@ export const layerWith = (options?: LayerOptions) =>
}), }),
) )
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Seq>> => { const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Event.Seq>> => {
if (aggregateIDs.length === 0) return Effect.succeed(new Map()) if (aggregateIDs.length === 0) return Effect.succeed(new Map())
return db return db
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq }) .select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
@ -674,7 +666,7 @@ export const layerWith = (options?: LayerOptions) =>
.all() .all()
.pipe( .pipe(
Effect.orDie, Effect.orDie,
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))), Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))),
) )
} }
@ -687,11 +679,11 @@ export const layerWith = (options?: LayerOptions) =>
}) })
}) })
const project = <D extends Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> => const project = <D extends Event.Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
Effect.sync(() => { Effect.sync(() => {
const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type
const list = projectors.get(key) ?? [] const list = projectors.get(key) ?? []
list.push((event) => projector(event as Payload<D>)) list.push((event) => projector(event as Event.Payload<D>))
projectors.set(key, list) projectors.set(key, list)
}) })

View file

@ -3,82 +3,82 @@ export * as Catalog from "./catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Order, pipe } from "effect" import { Array, Context, Effect, Layer, Order, pipe } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog" import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model" import { Model } from "./model"
import { ProviderV2 } from "./provider" import { Provider } from "./provider"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { State } from "./state" import { State } from "./state"
import { Integration } from "./integration" import { Integration } from "./integration"
export type ProviderRecord = { export type ProviderRecord = {
provider: ProviderV2.MutableInfo provider: Provider.MutableInfo
models: Map<ModelV2.ID, ModelV2.MutableInfo> models: Map<Model.ID, Model.MutableInfo>
} }
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID }
export const Event = Catalog.Event export { Event } from "@opencode-ai/schema/catalog"
type Data = { type Data = {
providers: Map<ProviderV2.ID, ProviderRecord> providers: Map<Provider.ID, ProviderRecord>
defaultModel?: DefaultModel defaultModel?: DefaultModel
} }
export type Draft = { export type Draft = {
provider: { provider: {
list: () => readonly ProviderRecord[] list: () => readonly ProviderRecord[]
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined get: (providerID: Provider.ID) => ProviderRecord | undefined
update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID) => void remove: (providerID: Provider.ID) => void
} }
model: { model: {
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined get: (providerID: Provider.ID, modelID: Model.ID) => Model.Info | undefined
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void update: (providerID: Provider.ID, modelID: Model.ID, fn: (model: Model.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void remove: (providerID: Provider.ID, modelID: Model.ID) => void
default: { default: {
get: () => DefaultModel | undefined get: () => DefaultModel | undefined
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void set: (providerID: Provider.ID, modelID: Model.ID) => void
} }
} }
} }
export interface Interface extends State.Transformable<Draft> { export interface Interface extends State.Transformable<Draft> {
readonly provider: { readonly provider: {
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined> readonly get: (providerID: Provider.ID) => Effect.Effect<Provider.Info | undefined>
readonly all: () => Effect.Effect<ProviderV2.Info[]> readonly all: () => Effect.Effect<Provider.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]> readonly available: () => Effect.Effect<Provider.Info[]>
} }
readonly model: { readonly model: {
readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined> readonly get: (providerID: Provider.ID, modelID: Model.ID) => Effect.Effect<Model.Info | undefined>
readonly all: () => Effect.Effect<ModelV2.Info[]> readonly all: () => Effect.Effect<Model.Info[]>
readonly available: () => Effect.Effect<ModelV2.Info[]> readonly available: () => Effect.Effect<Model.Info[]>
readonly default: () => Effect.Effect<ModelV2.Info | undefined> readonly default: () => Effect.Effect<Model.Info | undefined>
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined> readonly small: (providerID: Provider.ID) => Effect.Effect<Model.Info | undefined>
} }
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Catalog") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration return provider.integrationID === undefined && !integration
} }
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { const projectModel = (model: Model.Info, provider: Provider.Info) => {
return { return {
...model, ...model,
package: model.package ?? provider.package, package: model.package ?? provider.package,
settings: ProviderV2.mergeOverlay(provider.settings, model.settings), settings: Provider.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers), headers: Provider.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body), body: Provider.mergeOverlay(provider.body, model.body),
} satisfies ModelV2.Info } satisfies Model.Info
} }
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
@ -93,8 +93,8 @@ const layer = Layer.effect(
let current = draft.providers.get(providerID) let current = draft.providers.get(providerID)
if (!current) { if (!current) {
current = { current = {
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(), models: new Map<Model.ID, Model.MutableInfo>(),
} }
draft.providers.set(providerID, current) draft.providers.set(providerID, current)
} }
@ -110,13 +110,13 @@ const layer = Layer.effect(
let record = draft.providers.get(providerID) let record = draft.providers.get(providerID)
if (!record) { if (!record) {
record = { record = {
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(), models: new Map<Model.ID, Model.MutableInfo>(),
} }
draft.providers.set(providerID, record) draft.providers.set(providerID, record)
} }
const model = const model =
record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo) record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model) if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model) fn(model)
model.id = modelID model.id = modelID
@ -135,8 +135,8 @@ const layer = Layer.effect(
} }
return result return result
}, },
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) { finalize: Effect.fn("Catalog.finalize")(function* (catalog) {
yield* events.publish(Event.Updated, {}) yield* bus.publish(Catalog.Event.Updated, {})
}), }),
}) })
const result: Interface = { const result: Interface = {
@ -144,15 +144,15 @@ const layer = Layer.effect(
reload: state.reload, reload: state.reload,
provider: { provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { get: Effect.fn("Catalog.provider.get")(function* (providerID) {
return state.get().providers.get(providerID)?.provider return state.get().providers.get(providerID)?.provider
}), }),
all: Effect.fn("CatalogV2.provider.all")(function* () { all: Effect.fn("Catalog.provider.all")(function* () {
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
}), }),
available: Effect.fn("CatalogV2.provider.available")(function* () { available: Effect.fn("Catalog.provider.available")(function* () {
const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration])) const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
return (yield* result.provider.all()).filter((provider) => return (yield* result.provider.all()).filter((provider) =>
available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))), available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))),
@ -161,14 +161,14 @@ const layer = Layer.effect(
}, },
model: { model: {
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { get: Effect.fn("Catalog.model.get")(function* (providerID, modelID) {
const record = state.get().providers.get(providerID) const record = state.get().providers.get(providerID)
if (!record) return if (!record) return
const model = record.models.get(modelID) const model = record.models.get(modelID)
return model && projectModel(model, record.provider) return model && projectModel(model, record.provider)
}), }),
all: Effect.fn("CatalogV2.model.all")(function* () { all: Effect.fn("Catalog.model.all")(function* () {
return pipe( return pipe(
Array.fromIterable(state.get().providers.values()), Array.fromIterable(state.get().providers.values()),
Array.flatMap((record) => { Array.flatMap((record) => {
@ -178,9 +178,9 @@ const layer = Layer.effect(
) )
}), }),
available: Effect.fn("CatalogV2.model.available")(function* () { available: Effect.fn("Catalog.model.available")(function* () {
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
const models: ModelV2.Info[] = [] const models: Model.Info[] = []
for (const record of state.get().providers.values()) { for (const record of state.get().providers.values()) {
if (!providers.has(record.provider.id)) continue if (!providers.has(record.provider.id)) continue
for (const model of record.models.values()) { for (const model of record.models.values()) {
@ -194,7 +194,7 @@ const layer = Layer.effect(
) )
}), }),
default: Effect.fn("CatalogV2.model.default")(function* () { default: Effect.fn("Catalog.model.default")(function* () {
const defaultModel = state.get().defaultModel const defaultModel = state.get().defaultModel
if (defaultModel) { if (defaultModel) {
const provider = yield* result.provider.get(defaultModel.providerID) const provider = yield* result.provider.get(defaultModel.providerID)
@ -207,18 +207,18 @@ const layer = Layer.effect(
return (yield* result.model.available())[0] return (yield* result.model.available())[0]
}), }),
small: Effect.fn("CatalogV2.model.small")(function* (providerID) { small: Effect.fn("Catalog.model.small")(function* (providerID) {
const record = state.get().providers.get(providerID) const record = state.get().providers.get(providerID)
if (!record) return if (!record) return
const provider = record.provider const provider = record.provider
// TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments.
if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) {
return return
} }
if (providerID === ProviderV2.ID.opencode) { if (providerID === Provider.ID.opencode) {
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano"))
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
} }
@ -268,4 +268,4 @@ const layer = Layer.effect(
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] }) export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] })

View file

@ -1,77 +0,0 @@
export * as CodeMode from "./codemode"
import { Context, Effect, Layer, Scope } from "effect"
import { CodeModeCatalog } from "./codemode/catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { PermissionV2 } from "./permission"
import { ExecuteTool } from "./tool/execute"
import type { Any, Registration } from "./tool/tool"
import { Wildcard } from "./util/wildcard"
export interface Materialization {
readonly tool?: Any
readonly catalog?: ReadonlyArray<CodeModeCatalog.Entry>
}
export interface Interface {
readonly register: (
registrations: ReadonlyArray<Registration & { readonly key: string }>,
) => Effect.Effect<void, never, Scope.Scope>
readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect<Materialization>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/CodeMode") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const local = new Map<string, Array<{ readonly token: object; readonly registration: ExecuteTool.Registration }>>()
return Service.of({
register: Effect.fn("CodeMode.register")(function* (registrations) {
if (registrations.length === 0) return
yield* Effect.uninterruptible(
Effect.gen(function* () {
const token = {}
for (const registration of registrations)
local.set(registration.key, [
...(local.get(registration.key) ?? []),
{
token,
registration,
},
])
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
for (const registration of registrations) {
const remaining = local.get(registration.key)?.filter((item) => item.token !== token) ?? []
if (remaining.length > 0) local.set(registration.key, remaining)
else local.delete(registration.key)
}
}),
)
}),
)
}),
materialize: Effect.fn("CodeMode.materialize")(function* (permissions) {
const registrations = new Map<string, ExecuteTool.Registration>()
const rules = permissions ?? []
for (const [name, entries] of local) {
const registration = entries.at(-1)?.registration
if (!registration) continue
const rule = rules.findLast((rule) => Wildcard.match(registration.permission, rule.action))
if (rule?.resource === "*" && rule.effect === "deny") continue
registrations.set(name, registration)
}
const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action))
if (executeRule?.resource === "*" && executeRule.effect === "deny") return {}
return {
tool: ExecuteTool.create(registrations),
catalog: ExecuteTool.catalog(registrations),
}
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [] })

View file

@ -1,10 +1,9 @@
export * as ExecuteTool from "./execute" export * as CodeModeTool from "./tool"
export type { Registration } from "./tool"
import { CodeMode, Tool, toolError } from "@opencode-ai/codemode" import { CodeMode, Tool, toolError } from "@opencode-ai/codemode"
import type { ToolContent } from "@opencode-ai/ai" import type { Content, Context, Error, Info, Metadata, Result } from "@opencode-ai/schema/tool"
import { Effect, Ref, Schema, Semaphore } from "effect" import { Effect, Ref, Schema, Semaphore } from "effect"
import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool" import { definition } from "../tool/runtime"
const ExecuteFile = Schema.Struct({ const ExecuteFile = Schema.Struct({
data: Schema.String, data: Schema.String,
@ -42,8 +41,17 @@ const description = [
"Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.",
].join("\n") ].join("\n")
export const create = (registrations: ReadonlyMap<string, Registration>) => { export const create = (
return make({ registrations: ReadonlyMap<string, Info>,
executeTool: (
name: string,
tool: Info,
input: unknown,
context: Context,
) => Effect.Effect<Result, Error>,
) => {
return ({
name: "execute",
description, description,
input: CodeMode.Input, input: CodeMode.Input,
output: ExecuteOutput, output: ExecuteOutput,
@ -59,17 +67,17 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
) )
const result = yield* runtime( const result = yield* runtime(
registrations, registrations,
(name, registration, input) => (name, tool, input) =>
Effect.gen(function* () { Effect.gen(function* () {
const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1) const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1)
const executed = yield* execute(registration.tool, input, { const executed = yield* executeTool(name, tool, input, context).pipe(
sessionID: context.sessionID, Effect.mapError((failure) => toolError(failure.message, failure)),
agent: context.agent, )
messageID: context.messageID, const content =
callID: context.callID, typeof executed.content === "string"
progress: () => Effect.void, ? [{ type: "text" as const, text: executed.content }]
}).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) : executed.content ?? []
const outputFileParts = outputFiles(executed.content) const outputFileParts = outputFiles(content)
if (outputFileParts.length > 0) if (outputFileParts.length > 0)
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }]) yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
return executed.output return executed.output
@ -107,11 +115,11 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
files: collected, files: collected,
...(result.ok ? {} : { error: true }), ...(result.ok ? {} : { error: true }),
} }
const content: [Content, ...Content[]] = [{ type: "text", text: value.output }] const content: Array<Content> = [{ type: "text", text: value.output }]
content.push( content.push(
...value.files.map((file) => ({ ...value.files.map((file) => ({
type: "file" as const, type: "file" as const,
data: file.data, uri: `data:${file.mime};base64,${file.data}`,
mime: file.mime, mime: file.mime,
...(file.name === undefined ? {} : { name: file.name }), ...(file.name === undefined ? {} : { name: file.name }),
})), })),
@ -126,23 +134,26 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
metadata, metadata,
} }
}), }),
}) }) satisfies Info
} }
export const catalog = (registrations: ReadonlyMap<string, Registration>) => { export const catalog = (registrations: ReadonlyMap<string, Info>) => {
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog() return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog()
} }
function runtime( function runtime(
registrations: ReadonlyMap<string, Registration>, registrations: ReadonlyMap<string, Info>,
executeTool: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>, executeTool: (name: string, tool: Info, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks, hooks?: CodeMode.ToolCallHooks,
) { ) {
const tools: Record<string, Tool.Tool<never>> = {} const tools: Record<string, Tool.Tool<never>> = {}
for (const [name, registration] of registrations) { for (const [name, registration] of registrations) {
const child = toLLMDefinition(name, registration.tool) const child = definition(registration)
const normalized = registration.name.replace(/[^a-zA-Z0-9_-]/g, "_")
const path = const path =
registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}` registration.options?.namespace === undefined
? normalized
: `${registration.options.namespace}.${normalized}`
tools[path] = Tool.make({ tools[path] = Tool.make({
description: child.description, description: child.description,
input: child.inputSchema, input: child.inputSchema,
@ -180,7 +191,7 @@ function formatValue(value: CodeMode.DataValue) {
return JSON.stringify(value, null, 2) ?? String(value) return JSON.stringify(value, null, 2) ?? String(value)
} }
function outputFiles(content: ReadonlyArray<ToolContent>): Array<typeof ExecuteFile.Type> { function outputFiles(content: ReadonlyArray<Content>): Array<typeof ExecuteFile.Type> {
return content.flatMap((part) => { return content.flatMap((part) => {
if (part.type !== "file") return [] if (part.type !== "file") return []
const prefix = `data:${part.mime};base64,` const prefix = `data:${part.mime};base64,`

View file

@ -1,11 +1,11 @@
export * as CommandV2 from "./command" export * as Command from "./command"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema, Types } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { Command } from "@opencode-ai/schema/command" import { Command } from "@opencode-ai/schema/command"
import { State } from "./state" import { State } from "./state"
import { MCP } from "./mcp/index" import { MCP } from "./mcp/index"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { AppProcess } from "@opencode-ai/util/process" import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { Config } from "./config" import { Config } from "./config"
@ -14,7 +14,7 @@ import { ShellSelect } from "./shell/select"
export const Info = Command.Info export const Info = Command.Info
export type Info = Command.Info export type Info = Command.Info
export const Event = Command.Event export { Event } from "@opencode-ai/schema/command"
export type Evaluation = { export type Evaluation = {
readonly text: string readonly text: string
@ -50,13 +50,13 @@ export interface Interface extends State.Transformable<Draft> {
}) => Effect.Effect<Evaluation, NotFoundError | EvaluationError> }) => Effect.Effect<Evaluation, NotFoundError | EvaluationError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = (options?: ShellSelect.Options) => Layer.effect( export const layer = (options?: ShellSelect.Options) => Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const mcp = yield* MCP.Service const mcp = yield* MCP.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const processes = yield* AppProcess.Service const processes = yield* AppProcess.Service
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service const location = yield* Location.Service
@ -76,7 +76,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
draft.commands.delete(name) draft.commands.delete(name)
}, },
}), }),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
}) })
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () { const mcpCommands = Effect.fnUntraced(function* () {
@ -92,12 +92,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
return Service.of({ return Service.of({
reload: state.reload, reload: state.reload,
transform: state.transform, transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) { get: Effect.fn("Command.get")(function* (name) {
const command = staticCommand(name) const command = staticCommand(name)
if (command) return command if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name) return (yield* mcpCommands()).find((command) => command.name === name)
}), }),
list: Effect.fn("CommandV2.list")(function* () { list: Effect.fn("Command.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[] const commands = Array.from(state.get().commands.values()) as Info[]
const names = new Set(commands.map((command) => command.name)) const names = new Set(commands.map((command) => command.name))
return [ return [
@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
...(yield* mcpCommands()).filter((command) => !names.has(command.name)), ...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
] ]
}), }),
evaluate: Effect.fn("CommandV2.evaluate")(function* (input) { evaluate: Effect.fn("Command.evaluate")(function* (input) {
const command = staticCommand(input.name) const command = staticCommand(input.name)
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", { if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
config, config,
@ -247,7 +247,7 @@ export function configured(options?: ShellSelect.Options) {
return makeLocationNode({ return makeLocationNode({
service: Service, service: Service,
layer: layer(options), layer: layer(options),
deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node], deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node],
}) })
} }

View file

@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential" import { Credential } from "./credential"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { Watcher } from "./filesystem/watcher" import { Watcher } from "./filesystem/watcher"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
@ -168,7 +168,7 @@ export const Options = Schema.Struct({
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
export const layer = (options?: Options) => Layer.effect( export const layer = (options?: Options) => Layer.effect(
Service, Service,
@ -177,7 +177,7 @@ export const layer = (options?: Options) => Layer.effect(
const global = yield* Global.Service const global = yield* Global.Service
const location = yield* Location.Service const location = yield* Location.Service
const watcher = yield* Watcher.Service const watcher = yield* Watcher.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const credentials = yield* Credential.Service const credentials = yield* Credential.Service
const wellknown = yield* WellKnown.Service const wellknown = yield* WellKnown.Service
const names = ["opencode.json", "opencode.jsonc"] const names = ["opencode.json", "opencode.jsonc"]
@ -375,7 +375,7 @@ export const layer = (options?: Options) => Layer.effect(
if (isDeepStrictEqual(configs, next)) return if (isDeepStrictEqual(configs, next)) return
configs = next configs = next
yield* reconcile(next) yield* reconcile(next)
yield* events.publish(ConfigSchema.Event.Updated, {}) yield* bus.publish(ConfigSchema.Event.Updated, {})
}), }),
), ),
) )
@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect(
), ),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) => Stream.filterEffect((event) =>
wellknown.entries().pipe( wellknown.entries().pipe(
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)), Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
@ -401,7 +401,7 @@ export const layer = (options?: Options) => Layer.effect(
), ),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
yield* events.subscribe(WellKnown.Event.Updated).pipe( yield* bus.subscribe(WellKnown.Event.Updated).pipe(
Stream.runForEach(() => Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))), reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
), ),
@ -438,7 +438,7 @@ export function configured(options?: Options) {
return makeLocationNode({ return makeLocationNode({
service: Service, service: Service,
layer: layer(options), layer: layer(options),
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
}) })
} }

View file

@ -8,7 +8,7 @@ import { PositiveInt } from "../schema"
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({ export class Info extends Schema.Class<Info>("Config.Agent")({
model: ConfigModel.Selection.pipe(Schema.optional), model: ConfigModel.Selection.pipe(Schema.optional),
request: ConfigProvider.Request.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional),
system: Schema.String.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional),

View file

@ -3,13 +3,13 @@ export * as ConfigAttachments from "./attachments"
import { Schema } from "effect" import { Schema } from "effect"
import { PositiveInt } from "../schema" import { PositiveInt } from "../schema"
export class Image extends Schema.Class<Image>("ConfigV2.Attachments.Image")({ export class Image extends Schema.Class<Image>("Config.Attachments.Image")({
auto_resize: Schema.Boolean.pipe(Schema.optional), auto_resize: Schema.Boolean.pipe(Schema.optional),
max_width: PositiveInt.pipe(Schema.optional), max_width: PositiveInt.pipe(Schema.optional),
max_height: PositiveInt.pipe(Schema.optional), max_height: PositiveInt.pipe(Schema.optional),
max_base64_bytes: PositiveInt.pipe(Schema.optional), max_base64_bytes: PositiveInt.pipe(Schema.optional),
}) {} }) {}
export class Info extends Schema.Class<Info>("ConfigV2.Attachments")({ export class Info extends Schema.Class<Info>("Config.Attachments")({
image: Image.pipe(Schema.optional), image: Image.pipe(Schema.optional),
}) {} }) {}

View file

@ -3,7 +3,7 @@ export * as ConfigCommand from "./command"
import { Schema } from "effect" import { Schema } from "effect"
import { ConfigModel } from "./model" import { ConfigModel } from "./model"
export class Info extends Schema.Class<Info>("ConfigV2.Command")({ export class Info extends Schema.Class<Info>("Config.Command")({
template: Schema.String, template: Schema.String,
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional), agent: Schema.String.pipe(Schema.optional),

View file

@ -3,11 +3,11 @@ export * as ConfigCompaction from "./compaction"
import { Schema } from "effect" import { Schema } from "effect"
import { NonNegativeInt } from "../schema" import { NonNegativeInt } from "../schema"
export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({ export class Keep extends Schema.Class<Keep>("Config.Compaction.Keep")({
tokens: NonNegativeInt.pipe(Schema.optional), tokens: NonNegativeInt.pipe(Schema.optional),
}) {} }) {}
export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({ export class Info extends Schema.Class<Info>("Config.Compaction")({
auto: Schema.Boolean.pipe(Schema.optional), auto: Schema.Boolean.pipe(Schema.optional),
keep: Keep.pipe(Schema.optional), keep: Keep.pipe(Schema.optional),
buffer: NonNegativeInt.pipe(Schema.optional), buffer: NonNegativeInt.pipe(Schema.optional),

View file

@ -2,7 +2,7 @@ export * as ConfigFormatter from "./formatter"
import { Schema } from "effect" import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("ConfigV2.Formatter.Entry")({ export class Entry extends Schema.Class<Entry>("Config.Formatter.Entry")({
disabled: Schema.Boolean.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional),
command: Schema.String.pipe(Schema.Array, Schema.optional), command: Schema.String.pipe(Schema.Array, Schema.optional),
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),

View file

@ -6,7 +6,7 @@ export const Disabled = Schema.Struct({
disabled: Schema.Literal(true), disabled: Schema.Literal(true),
}) })
export class Server extends Schema.Class<Server>("ConfigV2.LSP.Server")({ export class Server extends Schema.Class<Server>("Config.LSP.Server")({
command: Schema.String.pipe(Schema.Array), command: Schema.String.pipe(Schema.Array),
extensions: Schema.String.pipe(Schema.Array, Schema.optional), extensions: Schema.String.pipe(Schema.Array, Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional),

View file

@ -15,7 +15,7 @@ export const Remote = Mcp.RemoteConfig
export type Remote = Mcp.RemoteConfig export type Remote = Mcp.RemoteConfig
export const Server = Mcp.ServerConfig export const Server = Mcp.ServerConfig
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({ export class Info extends Schema.Class<Info>("Config.MCP")({
timeout: Timeout.pipe(Schema.optional), timeout: Timeout.pipe(Schema.optional),
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
}) {} }) {}

View file

@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin"
import { Schema } from "effect" import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("ConfigV2.Plugin.Entry")({ export class Entry extends Schema.Class<Entry>("Config.Plugin.Entry")({
package: Schema.String, package: Schema.String,
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}) {} }) {}

View file

@ -1,9 +1,9 @@
export * as ConfigAgentPlugin from "./agent" export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path" import path from "path"
import { Effect, Option, Schema, Stream } from "effect" import { Effect, Option, Schema, Stream } from "effect"
import { AgentV2 } from "../../agent" import { Agent } from "../../agent"
import { Config } from "../../config" import { Config } from "../../config"
import { ConfigAgent } from "../agent" import { ConfigAgent } from "../agent"
import { ConfigMarkdown } from "../markdown" import { ConfigMarkdown } from "../markdown"
@ -11,10 +11,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigAgentV1 } from "../../v1/config/agent"
import { ConfigMigrateV1 } from "../../v1/config/migrate" import { ConfigMigrateV1 } from "../../v1/config/migrate"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
import { PermissionV2 } from "../../permission" import { Permission } from "../../permission"
import type { LocationMutation } from "../../location-mutation" import type { LocationMutation } from "../../location-mutation"
import type { ReadTool } from "../../tool/read" import type { ReadTool } from "../../tool/plugin/read"
import type { EditTool } from "../../tool/edit" import type { EditTool } from "../../tool/plugin/edit"
const legacySources = [ const legacySources = [
{ pattern: "{agent,agents}/**/*.md", primary: false }, { pattern: "{agent,agents}/**/*.md", primary: false },
@ -74,14 +74,14 @@ export const Plugin = define({
global.home, global.home,
) )
const configuredDefault = Config.latest(loaded.documents, "default_agent") const configuredDefault = Config.latest(loaded.documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault))
for (const current of draft.list()) { for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...permissions)) draft.update(current.id, (agent) => agent.permissions.push(...permissions))
} }
for (const document of loaded.documents) { for (const document of loaded.documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) { for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id) const agentID = Agent.ID.make(id)
if (item.disabled) { if (item.disabled) {
draft.remove(agentID) draft.remove(agentID)
continue continue
@ -126,7 +126,7 @@ export const Plugin = define({
}), }),
}) })
function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset {
// Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text:
// rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing.
return rules.map((rule) => return rules.map((rule) =>

View file

@ -1,9 +1,9 @@
export * as ConfigCommandPlugin from "./command" export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path" import path from "path"
import { Effect, Option, Schema, Stream } from "effect" import { Effect, Option, Schema, Stream } from "effect"
import { CommandV2 } from "../../command" import { Command } from "../../command"
import { Config } from "../../config" import { Config } from "../../config"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigCommand } from "../command" import { ConfigCommand } from "../command"

View file

@ -1,6 +1,6 @@
export * as ConfigPolicyPlugin from "./policy" export * as ConfigPolicyPlugin from "./policy"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { Wildcard } from "../../util/wildcard" import { Wildcard } from "../../util/wildcard"

View file

@ -1,10 +1,10 @@
export * as ConfigProviderPlugin from "./provider" export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ProviderV2 } from "../../provider" import { Provider } from "../../provider"
export const Plugin = define({ export const Plugin = define({
id: "opencode.config.provider", id: "opencode.config.provider",
@ -49,9 +49,9 @@ export const Plugin = define({
if (item.name !== undefined) provider.name = item.name if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) provider.package = item.package if (item.package !== undefined) provider.package = item.package
if (item.settings !== undefined) if (item.settings !== undefined)
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers) if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body) if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
}) })
for (const [id, config] of Object.entries(item.models ?? {})) { for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => { catalog.model.update(providerID, id, (model) => {
@ -62,9 +62,9 @@ export const Plugin = define({
model.compatibility = { ...model.compatibility, ...config.compatibility } model.compatibility = { ...model.compatibility, ...config.compatibility }
if (config.package !== undefined) model.package = config.package if (config.package !== undefined) model.package = config.package
if (config.settings !== undefined) if (config.settings !== undefined)
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings) model.settings = Provider.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers) if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body) if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
if (config.capabilities !== undefined) { if (config.capabilities !== undefined) {
model.capabilities = { model.capabilities = {
tools: config.capabilities.tools, tools: config.capabilities.tools,
@ -81,10 +81,10 @@ export const Plugin = define({
model.variants.push(existing) model.variants.push(existing)
} }
if (variant.settings !== undefined) if (variant.settings !== undefined)
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings) existing.settings = Provider.mergeOverlay(existing.settings, variant.settings)
if (variant.headers !== undefined) if (variant.headers !== undefined)
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers) existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body) if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
} }
} }
if (config.cost !== undefined) { if (config.cost !== undefined) {

View file

@ -1,6 +1,6 @@
export * as ConfigReferencePlugin from "./reference" export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path" import path from "path"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"

View file

@ -1,11 +1,11 @@
export * as ConfigSkillPlugin from "./skill" export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path" import path from "path"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { AbsolutePath } from "../../schema" import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill" import { Skill } from "../../skill"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
import { Location } from "../../location" import { Location } from "../../location"
@ -23,7 +23,7 @@ export const Plugin = define({
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of [...claude, ...agents]) { for (const directory of [...claude, ...agents]) {
draft.source( draft.source(
SkillV2.DirectorySource.make({ Skill.DirectorySource.make({
type: "directory", type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")), path: AbsolutePath.make(path.join(directory, "skills")),
}), }),
@ -31,10 +31,10 @@ export const Plugin = define({
} }
for (const directory of directories) { for (const directory of directories) {
draft.source( draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
) )
draft.source( draft.source(
SkillV2.DirectorySource.make({ Skill.DirectorySource.make({
type: "directory", type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")), path: AbsolutePath.make(path.join(directory, "skills")),
}), }),
@ -42,12 +42,12 @@ export const Plugin = define({
} }
for (const item of items) { for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) draft.source(Skill.UrlSource.make({ type: "url", url: item }))
continue continue
} }
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source( draft.source(
SkillV2.DirectorySource.make({ Skill.DirectorySource.make({
type: "directory", type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}), }),

View file

@ -1,6 +1,6 @@
export * as ConfigWebSearchPlugin from "./websearch" export * as ConfigWebSearchPlugin from "./websearch"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"

View file

@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider"
import { Schema } from "effect" import { Schema } from "effect"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { ModelV2 } from "../model" import { Capabilities, Compatibility, Family, ID, VariantID } from "../model"
const JsonRecord = Schema.Record(Schema.String, Schema.Json) const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@ -12,17 +12,17 @@ export const Overlays = {
body: JsonRecord.pipe(Schema.optional), body: JsonRecord.pipe(Schema.optional),
} }
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({ export class Request extends Schema.Class<Request>("Config.Provider.Request")({
headers: Overlays.headers, headers: Overlays.headers,
body: Overlays.body, body: Overlays.body,
}) {} }) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({ class Cache extends Schema.Class<Cache>("Config.Model.Cost.Cache")({
read: Money.USDPerMillionTokens.pipe(Schema.optional), read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional), write: Money.USDPerMillionTokens.pipe(Schema.optional),
}) {} }) {}
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({ class Cost extends Schema.Class<Cost>("Config.Model.Cost")({
tier: Schema.Struct({ tier: Schema.Struct({
type: Schema.Literal("context"), type: Schema.Literal("context"),
size: Schema.Int, size: Schema.Int,
@ -32,22 +32,22 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
cache: Cache.pipe(Schema.optional), cache: Cache.pipe(Schema.optional),
}) {} }) {}
class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({ class Limit extends Schema.Class<Limit>("Config.Model.Limit")({
context: Schema.Int.pipe(Schema.optional), context: Schema.Int.pipe(Schema.optional),
input: Schema.Int.pipe(Schema.optional), input: Schema.Int.pipe(Schema.optional),
output: Schema.Int.pipe(Schema.optional), output: Schema.Int.pipe(Schema.optional),
}) {} }) {}
class Model extends Schema.Class<Model>("ConfigV2.Model")({ class Model extends Schema.Class<Model>("Config.Model")({
modelID: ModelV2.ID.pipe(Schema.optional), modelID: ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional), family: Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional),
compatibility: ModelV2.Compatibility.pipe(Schema.optional), compatibility: Compatibility.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional),
...Overlays, ...Overlays,
capabilities: ModelV2.Capabilities.pipe(Schema.optional), capabilities: Capabilities.pipe(Schema.optional),
variants: Schema.Struct({ variants: Schema.Struct({
id: ModelV2.VariantID, id: VariantID,
...Overlays, ...Overlays,
}).pipe(Schema.Array, Schema.optional), }).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
@ -55,7 +55,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
limit: Limit.pipe(Schema.optional), limit: Limit.pipe(Schema.optional),
}) {} }) {}
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({ export class Info extends Schema.Class<Info>("Config.Provider")({
name: Schema.String.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional),
package: Schema.String.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional),

View file

@ -2,14 +2,14 @@ export * as ConfigReference from "./reference"
import { Schema } from "effect" import { Schema } from "effect"
export class Git extends Schema.Class<Git>("ConfigV2.Reference.Git")({ export class Git extends Schema.Class<Git>("Config.Reference.Git")({
repository: Schema.String, repository: Schema.String,
branch: Schema.String.pipe(Schema.optional), branch: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional),
}) {} }) {}
export class Local extends Schema.Class<Local>("ConfigV2.Reference.Local")({ export class Local extends Schema.Class<Local>("Config.Reference.Local")({
path: Schema.String, path: Schema.String,
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional),

View file

@ -3,7 +3,7 @@ export * as ConfigToolOutput from "./tool-output"
import { Schema } from "effect" import { Schema } from "effect"
import { PositiveInt } from "../schema" import { PositiveInt } from "../schema"
export class Info extends Schema.Class<Info>("ConfigV2.ToolOutput")({ export class Info extends Schema.Class<Info>("Config.ToolOutput")({
max_lines: PositiveInt.pipe(Schema.optional), max_lines: PositiveInt.pipe(Schema.optional),
max_bytes: PositiveInt.pipe(Schema.optional), max_bytes: PositiveInt.pipe(Schema.optional),
}) {} }) {}

View file

@ -2,7 +2,7 @@ export * as ConfigWarming from "./warming"
import { Schema } from "effect" import { Schema } from "effect"
export class Info extends Schema.Class<Info>("ConfigV2.Warming")({ export class Info extends Schema.Class<Info>("Config.Warming")({
prompt: Schema.String.pipe(Schema.optional).annotate({ prompt: Schema.String.pipe(Schema.optional).annotate({
description: "Prompt sent for keep-alive requests", description: "Prompt sent for keep-alive requests",
}), }),

View file

@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher"
import { Schema } from "effect" import { Schema } from "effect"
export class Info extends Schema.Class<Info>("ConfigV2.Watcher")({ export class Info extends Schema.Class<Info>("Config.Watcher")({
ignore: Schema.String.pipe(Schema.Array, Schema.optional), ignore: Schema.String.pipe(Schema.Array, Schema.optional),
}) {} }) {}

View file

@ -5,8 +5,8 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git" import { Git } from "../git"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
import { ProjectV2 } from "../project" import { Project } from "../project"
import { SessionV2 } from "../session" import { Session } from "../session"
import { SessionExecution } from "../session/execution" import { SessionExecution } from "../session/execution"
import { SessionSchema } from "../session/schema" import { SessionSchema } from "../session/schema"
import { SessionStore } from "../session/store" import { SessionStore } from "../session/store"
@ -28,8 +28,8 @@ export type Input = typeof Input.Type
export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()( export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
"MoveSession.DestinationProjectMismatchError", "MoveSession.DestinationProjectMismatchError",
{ {
expected: ProjectV2.ID, expected: Project.ID,
actual: ProjectV2.ID, actual: Project.ID,
}, },
) {} ) {}
@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSource
) {} ) {}
export type Error = export type Error =
| SessionV2.NotFoundError | Session.NotFoundError
| DestinationProjectMismatchError | DestinationProjectMismatchError
| DestinationNotFoundError | DestinationNotFoundError
| DestinationNotDirectoryError | DestinationNotDirectoryError
| SessionV2.DestinationNotFoundError | Session.DestinationNotFoundError
| SessionV2.DestinationNotDirectoryError | Session.DestinationNotDirectoryError
| CaptureChangesError | CaptureChangesError
| ApplyChangesError | ApplyChangesError
| ResetSourceChangesError | ResetSourceChangesError
@ -86,14 +86,14 @@ const layer = Layer.effect(
const git = yield* Git.Service const git = yield* Git.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const global = yield* Global.Service const global = yield* Global.Service
const project = yield* ProjectV2.Service const project = yield* Project.Service
const sessions = yield* SessionStore.Service const sessions = yield* SessionStore.Service
const session = yield* SessionV2.Service const session = yield* Session.Service
const execution = yield* SessionExecution.Service const execution = yield* SessionExecution.Service
const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) { const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
const current = yield* sessions.get(input.sessionID) const current = yield* sessions.get(input.sessionID)
if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID }) if (!current) return yield* new Session.NotFoundError({ sessionID: input.sessionID })
const value = input.destination.directory.trim() const value = input.destination.directory.trim()
const expanded = value === "~" ? global.home : value.startsWith("~/") ? path.join(global.home, value.slice(2)) : value const expanded = value === "~" ? global.home : value.startsWith("~/") ? path.join(global.home, value.slice(2)) : value
const directory = AbsolutePath.make(path.resolve(current.location.directory, expanded)) const directory = AbsolutePath.make(path.resolve(current.location.directory, expanded))
@ -173,8 +173,8 @@ export const node = makeGlobalNode({
FSUtil.node, FSUtil.node,
Git.node, Git.node,
Global.node, Global.node,
ProjectV2.node, Project.node,
SessionV2.node, Session.node,
SessionStore.node, SessionStore.node,
SessionExecution.node, SessionExecution.node,
], ],

View file

@ -1,17 +1,17 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/sql" import { ProjectTable } from "../project/sql"
import { ProjectV2 } from "../project" import { Project } from "../project"
import { WorkspaceV2 } from "../workspace" import { Workspace } from "../workspace"
export const WorkspaceTable = sqliteTable("workspace", { export const WorkspaceTable = sqliteTable("workspace", {
id: text().$type<WorkspaceV2.ID>().primaryKey(), id: text().$type<Workspace.ID>().primaryKey(),
type: text().notNull(), type: text().notNull(),
name: text().notNull().default(""), name: text().notNull().default(""),
branch: text(), branch: text(),
directory: text(), directory: text(),
extra: text({ mode: "json" }), extra: text({ mode: "json" }),
project_id: text() project_id: text()
.$type<ProjectV2.ID>() .$type<Project.ID>()
.notNull() .notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }), .references(() => ProjectTable.id, { onDelete: "cascade" }),
time_used: integer() time_used: integer()

View file

@ -46,7 +46,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void> readonly remove: (id: ID) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Credential") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -20,7 +20,7 @@ export const Options = Schema.Struct({
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {} export class Service extends Context.Service<Service, Interface>()("@opencode/storage/Database") {}
const databaseLayer = Layer.effect( const databaseLayer = Layer.effect(
Service, Service,

View file

@ -2,7 +2,7 @@ export * as EventLogger from "./event-logger"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event" import { Bus } from "./bus"
const Types = new Set([ const Types = new Set([
"agent.updated", "agent.updated",
@ -13,12 +13,12 @@ const Types = new Set([
export const layer = Layer.effectDiscard( export const layer = Layer.effectDiscard(
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const unsubscribe = yield* events.listen((event) => const unsubscribe = yield* bus.listen((event) =>
Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void, Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void,
) )
yield* Effect.addFinalizer(() => unsubscribe) yield* Effect.addFinalizer(() => unsubscribe)
}), }),
) )
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [EventV2.node] }) export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] })

View file

@ -1,5 +1,5 @@
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core"
import type { EventV2 } from "../event" import { Event } from "@opencode-ai/schema/event"
export const EventSequenceTable = sqliteTable("event_sequence", { export const EventSequenceTable = sqliteTable("event_sequence", {
aggregate_id: text().notNull().primaryKey(), aggregate_id: text().notNull().primaryKey(),
@ -10,7 +10,7 @@ export const EventSequenceTable = sqliteTable("event_sequence", {
export const EventTable = sqliteTable( export const EventTable = sqliteTable(
"event", "event",
{ {
id: text().$type<EventV2.ID>().primaryKey(), id: text().$type<Event.ID>().primaryKey(),
aggregate_id: text() aggregate_id: text()
.notNull() .notNull()
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),

View file

@ -64,7 +64,7 @@ export interface Interface {
readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, FSUtil.Error> readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, FSUtil.Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileMutation") {}
/** /**
* Serialize file changes by canonical target. Conditional writes compare and * Serialize file changes by canonical target. Conditional writes compare and
@ -194,12 +194,12 @@ function sameBytes(left: Uint8Array, right: Uint8Array) {
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
/** /**
* Deferred until the corresponding V2 integrations exist. * Deferred until the corresponding integrations exist.
*/ */
// TODO: Add formatter integration after V2 formatter runtime exists. // TODO: Add formatter integration after formatter runtime exists.
// TODO: Publish watcher/file-edit events after V2 watcher integration exists. // TODO: Publish watcher/file-edit events after watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists. // TODO: Add snapshots / undo after snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. // TODO: Notify LSP and collect diagnostics after LSP runtime exists.
// TODO: Design multi-file transactions / rollback if 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.

View file

@ -56,7 +56,7 @@ export interface Interface {
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]> readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
const baseLayer = Layer.effect( const baseLayer = Layer.effect(
Service, Service,

View file

@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os" import os from "os"
import path from "path" import path from "path"
import { Config } from "../config" import { Config } from "../config"
import { EventV2 } from "../event" import { Bus } from "../bus"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git" import { Git } from "../git"
import { Location } from "../location" import { Location } from "../location"
@ -30,12 +30,12 @@ const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const location = yield* Location.Service const location = yield* Location.Service
const watcher = yield* Watcher.Service const watcher = yield* Watcher.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const git = yield* Git.Service const git = yield* Git.Service
const configService = yield* Config.Service const configService = yield* Config.Service
const publish = (update: { type: "create" | "update" | "delete"; path: string }) => const publish = (update: { type: "create" | "update" | "delete"; path: string }) =>
events.publish(FileSystem.Event.Changed, { bus.publish(FileSystem.Event.Changed, {
file: update.path, file: update.path,
event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink", event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink",
}) })
@ -86,5 +86,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({ export const node = makeLocationNode({
service: Service, service: Service,
layer, layer,
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node], deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node],
}) })

View file

@ -22,7 +22,7 @@ export const Options = Schema.Struct({
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem/Search") {}
export const ripgrepLayer = Layer.effect( export const ripgrepLayer = Layer.effect(
Service, Service,

View file

@ -3,7 +3,7 @@ export * as Form from "./form"
import { Form } from "@opencode-ai/schema/form" import { Form } from "@opencode-ai/schema/form"
import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect" import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event" import { Bus } from "./bus"
const RETENTION = Duration.minutes(10) const RETENTION = Duration.minutes(10)
@ -32,7 +32,7 @@ export type Answer = typeof Answer.Type
export const Reply = Form.Reply export const Reply = Form.Reply
export type Reply = typeof Reply.Type export type Reply = typeof Reply.Type
export const Event = Form.Event export { Event } from "@opencode-ai/schema/form"
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", { export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", {
id: ID, id: ID,
@ -99,7 +99,7 @@ interface Entry {
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const forms = yield* Cache.makeWith<ID, Entry>( const forms = yield* Cache.makeWith<ID, Entry>(
() => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")), () => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")),
{ {
@ -141,7 +141,7 @@ export const layer = Layer.effect(
deferred: yield* Deferred.make<TerminalState>(), deferred: yield* Deferred.make<TerminalState>(),
} }
yield* Cache.set(forms, id, entry) yield* Cache.set(forms, id, entry)
yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id))) yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
return form return form
}), }),
), ),
@ -183,7 +183,7 @@ export const layer = Layer.effect(
const invalid = validateAnswer(entry.form, input.answer) const invalid = validateAnswer(entry.form, input.answer)
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid }) if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
const next: TerminalState = { status: "answered", answer: input.answer } const next: TerminalState = { status: "answered", answer: input.answer }
yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* Cache.set(forms, input.id, { ...entry, state: next }) yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next) yield* Deferred.succeed(entry.deferred, next)
}), }),
@ -196,7 +196,7 @@ export const layer = Layer.effect(
const entry = yield* find(id) const entry = yield* find(id)
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id }) if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id })
const next: TerminalState = { status: "cancelled" } const next: TerminalState = { status: "cancelled" }
yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID }) yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* Cache.set(forms, id, { ...entry, state: next }) yield* Cache.set(forms, id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next) yield* Deferred.succeed(entry.deferred, next)
}), }),
@ -221,7 +221,7 @@ export const layer = Layer.effect(
export const locationLayer = layer export const locationLayer = layer
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
function validateAnswer(form: Info, answer: Answer) { function validateAnswer(form: Info, answer: Answer) {
const fields = new Map(form.fields.map((field) => [field.key, field] as const)) const fields = new Map(form.fields.map((field) => [field.key, field] as const))

View file

@ -5,11 +5,11 @@ import { Context, Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { llmClient } from "./effect/app-node-platform" import { llmClient } from "./effect/app-node-platform"
import { ModelResolver } from "./model-resolver" import { ModelResolver } from "./model-resolver"
import { ModelV2 } from "./model" import { Model } from "./model"
export interface TextInput { export interface TextInput {
readonly prompt: string readonly prompt: string
readonly model?: ModelV2.Ref readonly model?: Model.Ref
} }
export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()( export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()(
@ -28,7 +28,7 @@ export interface Interface {
readonly text: (input: TextInput) => Effect.Effect<string, Error> readonly text: (input: TextInput) => Effect.Effect<string, Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Generate") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Generate") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,

View file

@ -189,7 +189,7 @@ export interface Interface {
} }
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -2,8 +2,8 @@ export * as CopilotModels from "./models"
import { Money } from "@opencode-ai/schema/money" import { Money } from "@opencode-ai/schema/money"
import { Option, Schema } from "effect" import { Option, Schema } from "effect"
import { ModelV2 } from "../model" import { Model } from "../model"
import { ProviderV2 } from "../provider" import { Provider } from "../provider"
const RemoteModel = Schema.Struct({ const RemoteModel = Schema.Struct({
model_picker_enabled: Schema.Boolean, model_picker_enabled: Schema.Boolean,
@ -70,7 +70,7 @@ type UsableModel = RemoteModel & {
} }
} }
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) { export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly Model.Info[]) {
const response = await fetch(`${baseURL}/models`, { const response = await fetch(`${baseURL}/models`, {
headers, headers,
signal: AbortSignal.timeout(5_000), signal: AbortSignal.timeout(5_000),
@ -97,7 +97,7 @@ export async function get(baseURL: string, headers: RequestInit["headers"], exis
} }
for (const [id, model] of remote) { for (const [id, model] of remote) {
const key = ModelV2.ID.make(id) const key = Model.ID.make(id)
if (result.has(key)) continue if (result.has(key)) continue
result.set(key, build(key, model, baseURL)) result.set(key, build(key, model, baseURL))
} }
@ -114,7 +114,7 @@ function usable(model: RemoteModel): model is UsableModel {
) )
} }
function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) { function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Model.Info) {
const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false
const endpoint = messages const endpoint = messages
? "messages" ? "messages"
@ -134,15 +134,15 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?:
: remote.version : remote.version
const released = previous?.time.released || Date.parse(version) const released = previous?.time.released || Date.parse(version)
return ModelV2.Info.make({ return Model.Info.make({
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id), ...Model.Info.default(Provider.ID.githubCopilot, id),
id, id,
modelID: ModelV2.ID.make(remote.id), modelID: Model.ID.make(remote.id),
providerID: ProviderV2.ID.githubCopilot, providerID: Provider.ID.githubCopilot,
family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family), family: previous?.family ?? Model.Family.make(remote.capabilities.family),
name: previous?.name ?? remote.name, name: previous?.name ?? remote.name,
package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: ProviderV2.mergeOverlay(previous?.settings, { settings: Provider.mergeOverlay(previous?.settings, {
baseURL: messages ? `${baseURL}/v1` : baseURL, baseURL: messages ? `${baseURL}/v1` : baseURL,
...(endpoint ? { endpoint } : {}), ...(endpoint ? { endpoint } : {}),
}), }),
@ -175,11 +175,11 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?:
}) })
} }
function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] { function variants(remote: UsableModel, messages: boolean): Model.Info["variants"] {
const efforts = remote.capabilities.supports.reasoning_effort ?? [] const efforts = remote.capabilities.supports.reasoning_effort ?? []
if (!messages && efforts.length) { if (!messages && efforts.length) {
return efforts.map((effort) => ({ return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort), id: Model.VariantID.make(effort),
settings: { settings: {
reasoningEffort: effort, reasoningEffort: effort,
reasoningSummary: "auto", reasoningSummary: "auto",
@ -189,7 +189,7 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant
} }
if (efforts.length && remote.capabilities.supports.adaptive_thinking) { if (efforts.length && remote.capabilities.supports.adaptive_thinking) {
return efforts.map((effort) => ({ return efforts.map((effort) => ({
id: ModelV2.VariantID.make(effort), id: Model.VariantID.make(effort),
settings: { settings: {
thinking: { thinking: {
type: "adaptive", type: "adaptive",
@ -203,11 +203,11 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant
if (max === undefined) return [] if (max === undefined) return []
return [ return [
{ {
id: ModelV2.VariantID.make("max"), id: Model.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: max - 1 } }, settings: { thinking: { type: "enabled", budgetTokens: max - 1 } },
}, },
{ {
id: ModelV2.VariantID.make("high"), id: Model.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } }, settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } },
}, },
] ]

View file

@ -26,7 +26,7 @@ export const Options = Schema.Struct({
}) })
export type Options = typeof Options.Type export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionDiscovery") {} export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionDiscovery") {}
export const layer = (options?: Options) => Layer.effect( export const layer = (options?: Options) => Layer.effect(
Service, Service,

View file

@ -10,7 +10,7 @@ export interface Interface {
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions> readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionBuiltIns") {} export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -20,7 +20,7 @@ import {
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential" import { Credential } from "./credential"
import { State } from "./state" import { State } from "./state"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { IntegrationConnection } from "./integration/connection" import { IntegrationConnection } from "./integration/connection"
import { AppProcess } from "@opencode-ai/util/process" import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
@ -129,7 +129,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError export type Error = CodeRequiredError | AuthorizationError
export const Event = Integration.Event export { Event } from "@opencode-ai/schema/integration"
export const Ref = Integration.Ref export const Ref = Integration.Ref
export type Ref = Integration.Ref export type Ref = Integration.Ref
@ -222,7 +222,7 @@ export interface Interface extends State.Transformable<Draft> {
} }
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Integration") {}
const attemptLifetime = Duration.toMillis(Duration.minutes(10)) const attemptLifetime = Duration.toMillis(Duration.minutes(10))
const terminalRetention = Duration.toMillis(Duration.minutes(1)) const terminalRetention = Duration.toMillis(Duration.minutes(1))
@ -271,7 +271,7 @@ const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const credentials = yield* Credential.Service const credentials = yield* Credential.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const processes = yield* AppProcess.Service const processes = yield* AppProcess.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>()) const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
@ -338,7 +338,7 @@ const layer = Layer.effect(
}, },
}, },
}), }),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid),
}) })
const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => { const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => {
@ -433,8 +433,8 @@ const layer = Layer.effect(
// Persisting attempts cannot be cancelled, expired, or claimed again. // Persisting attempts cannot be cancelled, expired, or claimed again.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal)) yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause) if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {}) yield* bus.publish(Integration.Event.Updated, {})
}).pipe(Effect.ensuring(close(attempt.scope))) }).pipe(Effect.ensuring(close(attempt.scope)))
}), }),
) )
@ -489,8 +489,8 @@ const layer = Layer.effect(
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal)) yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
yield* close(attempt.scope) yield* close(attempt.scope)
if (Exit.isFailure(persistence)) return if (Exit.isFailure(persistence)) return
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {}) yield* bus.publish(Integration.Event.Updated, {})
}), }),
) )
}) })
@ -706,24 +706,24 @@ const layer = Layer.effect(
label: input.label, label: input.label,
value: Credential.Key.make({ type: "key", key: input.key }), value: Credential.Key.make({ type: "key", key: input.key }),
}) })
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {}) yield* bus.publish(Integration.Event.Updated, {})
}), }),
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
const credential = yield* credentials.get(credentialID) const credential = yield* credentials.get(credentialID)
yield* credentials.update(credentialID, updates) yield* credentials.update(credentialID, updates)
if (credential) { if (credential) {
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
} }
yield* events.publish(Event.Updated, {}) yield* bus.publish(Integration.Event.Updated, {})
}), }),
remove: Effect.fn("Integration.connection.remove")(function* (credentialID) { remove: Effect.fn("Integration.connection.remove")(function* (credentialID) {
const credential = yield* credentials.get(credentialID) const credential = yield* credentials.get(credentialID)
yield* credentials.remove(credentialID) yield* credentials.remove(credentialID)
if (credential) { if (credential) {
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
} }
yield* events.publish(Event.Updated, {}) yield* bus.publish(Integration.Event.Updated, {})
}), }),
}, },
oauth: { oauth: {
@ -809,5 +809,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({ export const node = makeLocationNode({
service: Service, service: Service,
layer, layer,
deps: [Credential.node, EventV2.node, AppProcess.node], deps: [Credential.node, Bus.node, AppProcess.node],
}) })

View file

@ -14,7 +14,7 @@ export interface Interface {
readonly remove: (key: string) => Effect.Effect<void> readonly remove: (key: string) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {} export class Service extends Context.Service<Service, Interface>()("@opencode/KV") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -60,7 +60,7 @@ export interface Interface {
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error> readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationMutation") {} export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
interface ResolvedPath { interface ResolvedPath {
readonly canonical: string readonly canonical: string

View file

@ -1,13 +1,12 @@
import { Effect, Layer, LayerMap } from "effect" import { Effect, Layer, LayerMap } from "effect"
import { AgentV2 } from "./agent" import { Agent } from "./agent"
import { AISDK } from "./aisdk" import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
import { CodeMode } from "./codemode" import { Command } from "./command"
import { CommandV2 } from "./command"
import { Config } from "./config" import { Config } from "./config"
import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Node } from "@opencode-ai/util/effect/app-node" import { Node } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { FileMutation } from "./file-mutation" import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem" import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
@ -21,12 +20,12 @@ import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map" import { LocationServiceMap } from "./location-service-map"
import { ModelResolver } from "./model-resolver" import { ModelResolver } from "./model-resolver"
import { MCP } from "./mcp/index" import { MCP } from "./mcp/index"
import { PermissionV2 } from "./permission" import { Permission } from "./permission"
import { PluginV2 } from "./plugin" import { Plugin } from "./plugin"
import { PluginSupervisor } from "./plugin/supervisor" import { PluginSupervisor } from "./plugin/supervisor"
import { ProjectCopy } from "./project/copy" import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty" import { Pty } from "./pty"
import { QuestionV2 } from "./question" import { Question } from "./question"
import { Shell } from "./shell" import { Shell } from "./shell"
import { Reference } from "./reference" import { Reference } from "./reference"
import { WebSearch } from "./websearch" import { WebSearch } from "./websearch"
@ -35,7 +34,7 @@ import { SessionRunnerLLM } from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model" import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction" import { SessionCompaction } from "./session/compaction"
import { SessionTitle } from "./session/title" import { SessionTitle } from "./session/title"
import { SkillV2 } from "./skill" import { Skill } from "./skill"
import { SkillInstructions } from "./skill/instructions" import { SkillInstructions } from "./skill/instructions"
import { Snapshot } from "./snapshot" import { Snapshot } from "./snapshot"
import { InstructionDiscovery } from "./instruction-discovery" import { InstructionDiscovery } from "./instruction-discovery"
@ -45,8 +44,7 @@ import { SessionInstructions } from "./session/instructions"
import { SessionGenerateNode } from "./session/generate-node" import { SessionGenerateNode } from "./session/generate-node"
import { McpTool } from "./tool/mcp" import { McpTool } from "./tool/mcp"
import { ReadToolFileSystem } from "./tool/read-filesystem" import { ReadToolFileSystem } from "./tool/read-filesystem"
import { ToolRegistry } from "./tool/registry" import { Tool } from "./tool"
import { ToolOutputStore } from "./tool-output-store"
import { Vcs } from "./vcs" import { Vcs } from "./vcs"
export { LocationServiceMap } from "./location-service-map" export { LocationServiceMap } from "./location-service-map"
@ -54,15 +52,15 @@ export { LocationServiceMap } from "./location-service-map"
const locationServiceNodes = [ const locationServiceNodes = [
Location.node, Location.node,
Config.node, Config.node,
AgentV2.node, Agent.node,
CommandV2.node, Command.node,
Reference.node, Reference.node,
WebSearch.node, WebSearch.node,
Integration.node, Integration.node,
Catalog.node, Catalog.node,
ModelResolver.node, ModelResolver.node,
AISDK.node, AISDK.node,
PluginV2.node, Plugin.node,
PluginSupervisor.node, PluginSupervisor.node,
ProjectCopy.node, ProjectCopy.node,
ProjectCopy.refreshNode, ProjectCopy.refreshNode,
@ -70,23 +68,20 @@ const locationServiceNodes = [
FileSystem.node, FileSystem.node,
Pty.node, Pty.node,
Shell.node, Shell.node,
SkillV2.node, Skill.node,
CodeMode.node,
InstructionBuiltIns.node, InstructionBuiltIns.node,
InstructionDiscovery.node, InstructionDiscovery.node,
LocationMutation.node, LocationMutation.node,
FileMutation.node, FileMutation.node,
MCP.node, MCP.node,
PermissionV2.node, Permission.node,
ToolOutputStore.node, Tool.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
Image.node, Image.node,
SkillInstructions.node, SkillInstructions.node,
ReferenceInstructions.node, ReferenceInstructions.node,
InstructionEntry.node, InstructionEntry.node,
Form.node, Form.node,
QuestionV2.node, Question.node,
Generate.node, Generate.node,
SessionGenerateNode.node, SessionGenerateNode.node,
ReadToolFileSystem.node, ReadToolFileSystem.node,

View file

@ -9,7 +9,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Config } from "../config" import { Config } from "../config"
import { ConfigMCP } from "../config/mcp" import { ConfigMCP } from "../config/mcp"
import { Credential } from "../credential" import { Credential } from "../credential"
import { EventV2 } from "../event" import { Bus } from "../bus"
import { Form } from "../form" import { Form } from "../form"
import { Integration } from "../integration" import { Integration } from "../integration"
import { IntegrationConnection } from "../integration/connection" import { IntegrationConnection } from "../integration/connection"
@ -155,7 +155,7 @@ export interface Interface {
}) => Effect.Effect<ResourceContent | undefined, NotFoundError> }) => Effect.Effect<ResourceContent | undefined, NotFoundError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {} export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
export const Options = Schema.Struct({ export const Options = Schema.Struct({
clientInfo: Schema.optional( clientInfo: Schema.optional(
@ -172,7 +172,7 @@ export const layer = (options?: Options) => Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service const location = yield* Location.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const forms = yield* Form.Service const forms = yield* Form.Service
const integration = yield* Integration.Service const integration = yield* Integration.Service
const credentials = yield* Credential.Service const credentials = yield* Credential.Service
@ -433,9 +433,9 @@ export const layer = (options?: Options) => Layer.effect(
Effect.map((defs) => { Effect.map((defs) => {
entry.prompts = defs.map((def) => toPrompt(name, def)) entry.prompts = defs.map((def) => toPrompt(name, def))
}), }),
Effect.andThen(events.publish(Command.Event.Updated, {})), Effect.andThen(bus.publish(Command.Event.Updated, {})),
Effect.catch(() => Effect.catch(() =>
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))), Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))),
), ),
) )
@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined entry.tools = undefined
entry.prompts = undefined entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" } entry.status = { status: "failed", error: "Connection closed" }
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}), }),
), ),
) )
@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect(
connection.onToolsChanged(() => connection.onToolsChanged(() =>
live( live(
refreshTools(name, entry, connection).pipe( refreshTools(name, entry, connection).pipe(
Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })), Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })),
), ),
), ),
) )
connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection))) connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection)))
connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name }))) connection.onResourcesChanged(() => live(bus.publish(McpEvent.ResourcesChanged, { server: name })))
} }
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => { const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@ -502,7 +502,7 @@ export const layer = (options?: Options) => Layer.effect(
// Announce the handshake so connect() and credential reconnects don't show a stale // Announce the handshake so connect() and credential reconnects don't show a stale
// disabled/failed status for the duration of the connection attempt. // disabled/failed status for the duration of the connection attempt.
entry.status = { status: "pending" } entry.status = { status: "pending" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
const scope = yield* Scope.fork(root) const scope = yield* Scope.fork(root)
entry.scope = scope entry.scope = scope
const authProvider = yield* connectProvider(entry) const authProvider = yield* connectProvider(entry)
@ -530,9 +530,9 @@ export const layer = (options?: Options) => Layer.effect(
// Announce the new tool set so the tool registry registers it. A server that finishes connecting // Announce the new tool set so the tool registry registers it. A server that finishes connecting
// 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* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection)) whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection))
return return
} }
@ -544,7 +544,7 @@ export const layer = (options?: Options) => Layer.effect(
? { status: "needs_auth" } ? { status: "needs_auth" }
: { status: "failed", error: error instanceof Error ? error.message : String(error) } : { status: "failed", error: error instanceof Error ? error.message : String(error) }
yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status }) yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status })
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined))) }).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined)))
const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) { const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) {
@ -555,9 +555,9 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined entry.tools = undefined
entry.prompts = undefined entry.prompts = undefined
yield* Scope.close(scope, Exit.void) yield* Scope.close(scope, Exit.void)
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
}) })
// Disabled servers settle their startup immediately so queries never block on them. // Disabled servers settle their startup immediately so queries never block on them.
@ -587,7 +587,7 @@ export const layer = (options?: Options) => Layer.effect(
}).pipe(locks.withLock(name)) }).pipe(locks.withLock(name))
}) })
fork( fork(
events.subscribe(Integration.Event.ConnectionUpdated).pipe( bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => owned.has(event.data.integrationID)), Stream.filter((event) => owned.has(event.data.integrationID)),
Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))), Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))),
Effect.ignore, Effect.ignore,
@ -632,7 +632,7 @@ export const layer = (options?: Options) => Layer.effect(
yield* register(name, entry) yield* register(name, entry)
if (config.disabled) { if (config.disabled) {
entry.status = { status: "disabled" } entry.status = { status: "disabled" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
return return
} }
yield* startServer(name, entry) yield* startServer(name, entry)
@ -657,7 +657,7 @@ export const layer = (options?: Options) => Layer.effect(
const target = yield* requireServer(name) const target = yield* requireServer(name)
yield* stopServer(name, target.entry) yield* stopServer(name, target.entry)
target.entry.status = { status: "disabled" } target.entry.status = { status: "disabled" }
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name)) }).pipe(locks.withLock(name))
}), }),
remove: Effect.fn("MCP.remove")(function* (server) { remove: Effect.fn("MCP.remove")(function* (server) {
@ -670,7 +670,7 @@ export const layer = (options?: Options) => Layer.effect(
// Credentials are kept: they are keyed by name + url, so re-adding the same server // Credentials are kept: they are keyed by name + url, so re-adding the same server
// reuses them without forcing re-auth, matching add()'s replacement semantics. // reuses them without forcing re-auth, matching add()'s replacement semantics.
runtime.delete(name) runtime.delete(name)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name)) }).pipe(locks.withLock(name))
}), }),
tools: Effect.fn("MCP.tools")(function* () { tools: Effect.fn("MCP.tools")(function* () {
@ -793,7 +793,7 @@ export function configured(options?: Options) {
return makeLocationNode({ return makeLocationNode({
service: Service, service: Service,
layer: layer(options), layer: layer(options),
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node], deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
}) })
} }

View file

@ -2,8 +2,8 @@ export * as McpInstructions from "./instructions"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent" import { Agent } from "../agent"
import { PermissionV2 } from "../permission" import { Permission } from "../permission"
import { McpTool } from "../tool/mcp" import { McpTool } from "../tool/mcp"
import { MCP } from "./index" import { MCP } from "./index"
import { Instructions } from "../instructions/index" import { Instructions } from "../instructions/index"
@ -55,10 +55,10 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
} }
export interface Interface { export interface Interface {
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions> readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpInstructions") {} export class Service extends Context.Service<Service, Interface>()("@opencode/McpInstructions") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@ -83,7 +83,7 @@ export const layer = Layer.effect(
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], { const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded", concurrency: "unbounded",
}) })
const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny" const canExecute = Permission.evaluate("execute", "*", agent.permissions).effect !== "deny"
// Instructions are useful only when this agent can reach at least one server tool. // Instructions are useful only when this agent can reach at least one server tool.
const visible = instructions const visible = instructions
.flatMap((item) => { .flatMap((item) => {
@ -93,7 +93,7 @@ export const layer = Layer.effect(
if ( if (
!owned.some( !owned.some(
(tool) => (tool) =>
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", Permission.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
) )
) )
return [] return []

View file

@ -15,17 +15,17 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
import { Credential } from "./credential" import { Credential } from "./credential"
import { Integration } from "./integration" import { Integration } from "./integration"
import { ModelV2 } from "./model" import { Capabilities, ID, Info, Ref, VariantID } from "./model"
import { Npm } from "@opencode-ai/util/npm" import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex" import { OpenAICodex } from "./plugin/provider/openai-codex"
import { ProviderV2 } from "./provider" import { Provider } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()( export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError", "SessionRunnerModel.VariantUnavailableError",
{ {
providerID: ProviderV2.ID, providerID: Provider.ID,
modelID: ModelV2.ID, modelID: ID,
variant: ModelV2.VariantID, variant: VariantID,
}, },
) { ) {
override get message() { override get message() {
@ -36,8 +36,8 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()( export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError", "SessionRunnerModel.UnsupportedPackageError",
{ {
providerID: ProviderV2.ID, providerID: Provider.ID,
modelID: ModelV2.ID, modelID: ID,
package: Schema.String, package: Schema.String,
}, },
) { ) {
@ -52,21 +52,21 @@ export interface Resolved {
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
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: Ref
/** Catalog capabilities used to shape requests before provider lowering. */ /** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: ModelV2.Capabilities readonly capabilities: Capabilities
/** Catalog pricing in dollars per million tokens. */ /** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"] readonly cost: Info["cost"]
} }
export interface Interface { export interface Interface {
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error> readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error> readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { const apiKey = (model: Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key) if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access) if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey const value = model.settings?.apiKey
@ -74,7 +74,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
return undefined return undefined
} }
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => const withDefaults = (model: Info, route: AnyRoute) =>
route.with({ route.with({
provider: model.providerID, provider: model.providerID,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
@ -84,8 +84,8 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
limits: { context: model.limit.context, output: model.limit.output }, limits: { context: model.limit.context, output: model.limit.output },
}) })
const providerHeaders = (model: ModelV2.Info) => { const providerHeaders = (model: Info) => {
const packageName = ProviderV2.packageName(model.package) const packageName = Provider.packageName(model.package)
const generated = new Map<string, string>() const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization) generated.set("OpenAI-Organization", model.settings.organization)
@ -93,16 +93,16 @@ const providerHeaders = (model: ModelV2.Info) => {
generated.set("OpenAI-Project", model.settings.project) generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`) generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
} }
const providerOptions = ( const providerOptions = (
model: ModelV2.Info, model: Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { ): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined if (Object.keys(settings).length === 0) return undefined
const packageName = ProviderV2.packageName(model.package) const packageName = Provider.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings } if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
@ -110,9 +110,9 @@ const providerOptions = (
} }
export const withVariant = ( export const withVariant = (
model: ModelV2.Info, model: Info,
variantID: ModelV2.VariantID | undefined, variantID: VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => { ): Effect.Effect<Info, VariantUnavailableError> => {
const id = variantID === "default" ? undefined : variantID const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id) const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default") if (!variant && variantID !== undefined && variantID !== "default")
@ -126,37 +126,37 @@ export const withVariant = (
return Effect.succeed( return Effect.succeed(
variant variant
? produce(model, (draft) => { ? produce(model, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) draft.settings = Provider.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) draft.headers = Provider.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) draft.body = Provider.mergeOverlay(draft.body, variant.body)
}) })
: model, : model,
) )
} }
export interface Dependencies { export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError> readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError> readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
} }
export const fromCatalogModel = ( export const fromCatalogModel = (
model: ModelV2.Info, model: Info,
credential?: Credential.Value, credential?: Credential.Value,
dependencies?: Dependencies, dependencies?: Dependencies,
): Effect.Effect<Model, UnsupportedPackageError> => { ): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = produce(model, (draft) => { const resolved = produce(model, (draft) => {
if (draft.settings?.apiKey === "") delete draft.settings.apiKey if (draft.settings?.apiKey === "") delete draft.settings.apiKey
if (credential?.type === "key" && credential.metadata !== undefined) if (credential?.type === "key" && credential.metadata !== undefined)
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) draft.body = Provider.mergeOverlay(draft.body, credential.metadata)
}) })
const packageName = ProviderV2.packageName(resolved.package) const packageName = Provider.packageName(resolved.package)
const key = apiKey(resolved, credential) const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key)) return Effect.succeed(codexModel(resolved, credential, key))
} }
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed( return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route) withDefaults(resolved, OpenAIResponses.route)
@ -164,7 +164,7 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
) )
} }
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed( return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route) withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
@ -172,7 +172,7 @@ export const fromCatalogModel = (
) )
} }
if ( if (
ProviderV2.isAISDK(resolved.package) && Provider.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" && packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string" typeof resolved.settings?.baseURL === "string"
) { ) {
@ -182,10 +182,10 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
) )
} }
if (ProviderV2.isAISDK(resolved.package)) { if (Provider.isAISDK(resolved.package)) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => { const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, { draft.settings = Provider.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}), ...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata, ...credential?.metadata,
@ -197,7 +197,7 @@ export const fromCatalogModel = (
const specifier = resolved.package const specifier = resolved.package
return Effect.gen(function* () { return Effect.gen(function* () {
const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)), Effect.mapError(() => unsupported(resolved)),
) )
const configured = { ...resolved.settings, ...credential?.metadata } const configured = { ...resolved.settings, ...credential?.metadata }
@ -249,7 +249,7 @@ const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
} }
const codexModel = ( const codexModel = (
model: ModelV2.Info, model: Info,
credential: Credential.Value | undefined, credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined, key: ReturnType<typeof Auth.value> | undefined,
) => { ) => {
@ -264,7 +264,7 @@ const codexModel = (
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) .model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
} }
const unsupported = (model: ModelV2.Info) => const unsupported = (model: Info) =>
new UnsupportedPackageError({ new UnsupportedPackageError({
providerID: model.providerID, providerID: model.providerID,
modelID: model.id, modelID: model.id,
@ -272,13 +272,13 @@ const unsupported = (model: ModelV2.Info) =>
}) })
export const resolveModel = ( export const resolveModel = (
model: ModelV2.Info, model: Info,
variant: ModelV2.VariantID | undefined, variant: VariantID | undefined,
credential?: Credential.Value, credential?: Credential.Value,
dependencies?: Dependencies, dependencies?: Dependencies,
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies))) ) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const supported = (model: ModelV2.Info) => Boolean(model.package) export const supported = (model: Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */ /** Resolves catalog selections into runtime models for the current Location. */
export const layer = Layer.effect( export const layer = Layer.effect(
@ -289,8 +289,8 @@ export const layer = Layer.effect(
const npm = yield* Npm.Service const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
const load = Effect.fn("ModelResolver.resolveModel")(function* ( const load = Effect.fn("ModelResolver.resolveModel")(function* (
selected: ModelV2.Info, selected: Info,
variant?: ModelV2.VariantID, variant?: VariantID,
) { ) {
const provider = yield* catalog.provider.get(selected.providerID) const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active( const connection = yield* integrations.connection.active(
@ -301,13 +301,13 @@ export const layer = Layer.effect(
variant, variant,
connection ? yield* integrations.connection.resolve(connection) : undefined, connection ? yield* integrations.connection.resolve(connection) : undefined,
{ {
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model), loadAISDK: (model) => aisdk.model(model),
}, },
) )
return { return {
model, model,
ref: ModelV2.Ref.make({ ref: Ref.make({
id: selected.id, id: selected.id,
providerID: selected.providerID, providerID: selected.providerID,
...(variant === undefined ? {} : { variant }), ...(variant === undefined ? {} : { variant }),

View file

@ -1,5 +1,5 @@
import { Model } from "@opencode-ai/schema/model" import { Model } from "@opencode-ai/schema/model"
import { ProviderV2 } from "./provider" import { Provider } from "./provider"
import type { DeepMutable } from "./schema" import type { DeepMutable } from "./schema"
export const ID = Model.ID export const ID = Model.ID
@ -37,12 +37,12 @@ export function compatibility(input: unknown): Compatibility | undefined {
return typeof input.field === "string" ? { reasoningField: input.field } : undefined return typeof input.field === "string" ? { reasoningField: input.field } : undefined
} }
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { export function parse(input: string): { providerID: Provider.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/") const [providerID, ...modelID] = input.split("/")
return { return {
providerID: ProviderV2.ID.make(providerID), providerID: Provider.ID.make(providerID),
modelID: ID.make(modelID.join("/")), modelID: ID.make(modelID.join("/")),
} }
} }
export * as ModelV2 from "./model" export * as Model from "./model"

View file

@ -8,11 +8,11 @@ import { Global } from "@opencode-ai/util/global"
import { Flock } from "@opencode-ai/util/flock" import { Flock } from "@opencode-ai/util/flock"
import { Hash } from "@opencode-ai/util/hash" import { Hash } from "@opencode-ai/util/hash"
import { FSUtil } from "@opencode-ai/util/fs-util" import { FSUtil } from "@opencode-ai/util/fs-util"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { httpClient } from "@opencode-ai/util/effect/app-node-platform" import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { ModelV2 } from "./model" import { Model } from "./model"
import { ProviderV2 } from "./provider" import { Provider } from "./provider"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type export type CatalogModelStatus = typeof CatalogModelStatus.Type
@ -54,7 +54,7 @@ type SourceModel = {
{ {
readonly cost?: Cost readonly cost?: Cost
readonly provider?: { readonly provider?: {
readonly body?: ProviderV2.Settings readonly body?: Provider.Settings
readonly headers?: Readonly<Record<string, string>> readonly headers?: Readonly<Record<string, string>>
} }
} }
@ -75,29 +75,29 @@ type SourceProvider = {
} }
export type Snapshot = { export type Snapshot = {
readonly info: ProviderV2.Info readonly info: Provider.Info
readonly models: readonly ModelV2.Info[] readonly models: readonly Model.Info[]
readonly environment: readonly string[] readonly environment: readonly string[]
} }
function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] { function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
const providers: Snapshot[] = [] const providers: Snapshot[] = []
for (const item of Object.values(input)) { for (const item of Object.values(input)) {
const providerID = ProviderV2.ID.make(item.id) const providerID = Provider.ID.make(item.id)
const info = { const info = {
id: providerID, id: providerID,
name: item.name, name: item.name,
package: ProviderV2.aisdk(item.npm), package: Provider.aisdk(item.npm),
...(item.api ? { settings: { baseURL: item.api } } : {}), ...(item.api ? { settings: { baseURL: item.api } } : {}),
} satisfies ProviderV2.Info } satisfies Provider.Info
const models: ModelV2.Info[] = [] const models: Model.Info[] = []
for (const model of Object.values(item.models)) { for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost) const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model) const variants = reasoningVariants(item, model)
const id = ModelV2.ID.make(model.id) const id = Model.ID.make(model.id)
models.push(modelInfo(providerID, id, model, { cost: baseCost, variants })) models.push(modelInfo(providerID, id, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
const modeID = ModelV2.ID.make(`${model.id}-${mode}`) const modeID = Model.ID.make(`${model.id}-${mode}`)
models.push( models.push(
modelInfo(providerID, modeID, model, { modelInfo(providerID, modeID, model, {
name: modeName(model, mode), name: modeName(model, mode),
@ -118,7 +118,7 @@ function released(date: string) {
return Number.isFinite(time) ? time : 0 return Number.isFinite(time) ? time : 0
} }
function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] { function cost(input: SourceModel["cost"]): Model.Info["cost"] {
const base = { const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero, input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero, output: input?.output ?? Money.USDPerMillionTokens.zero,
@ -154,13 +154,13 @@ function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] {
] ]
} }
function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | undefined) { function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | undefined) {
if (!override) return base if (!override) return base
const next = cost(override) const next = cost(override)
const [baseDefault, ...baseTiers] = base const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelV2.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` const tierKey = (item: Model.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2.Info["cost"][number], right: ModelV2.Info["cost"][number]) => ({ const merge = (left: Model.Info["cost"][number], right: Model.Info["cost"][number]) => ({
...left, ...left,
...right, ...right,
tier: right.tier ?? left.tier, tier: right.tier ?? left.tier,
@ -187,7 +187,7 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
const OUTPUT_TOKEN_MAX = 32_000 const OUTPUT_TOKEN_MAX = 32_000
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<ModelV2.Info["variants"]> { function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<Model.Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options const options = model.reasoning_options
if (!options?.length) return [] if (!options?.length) return []
@ -203,7 +203,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
if (id === undefined) return [] if (id === undefined) return []
if (id === "none" && off.length > 0) return [] if (id === "none" && off.length > 0) return []
const settings = settingsForEffort(npm, model.id, id) const settings = settingsForEffort(npm, model.id, id)
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : [] return settings ? [{ id: Model.VariantID.make(id), settings }] : []
}), }),
] ]
return [...new Map(variants.map((variant) => [variant.id, variant])).values()] return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
@ -218,7 +218,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
return [] return []
} }
function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined { function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } } if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") { if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
if (anthropicManualThinking(modelID)) return { effort } if (anthropicManualThinking(modelID)) return { effort }
@ -287,7 +287,7 @@ function budgetVariants(
npm: string, npm: string,
model: SourceModel, model: SourceModel,
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>, option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<ModelV2.Info["variants"]> { ): NonNullable<Model.Info["variants"]> {
const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1) const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
if (maximum <= 0) return [] if (maximum <= 0) return []
const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum) const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
@ -296,35 +296,35 @@ function budgetVariants(
{ id: "max", budget: maximum }, { id: "max", budget: maximum },
].flatMap((item) => { ].flatMap((item) => {
const settings = settingsForBudget(npm, model.id, item.budget) const settings = settingsForBudget(npm, model.id, item.budget)
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : [] return settings ? [{ id: Model.VariantID.make(item.id), settings }] : []
}) })
} }
function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info["variants"]> { function toggleVariants(npm: string, modelID: string): NonNullable<Model.Info["variants"]> {
if (npm === "@ai-sdk/gateway") { if (npm === "@ai-sdk/gateway") {
const upstream = gatewayPackage(modelID) const upstream = gatewayPackage(modelID)
if (upstream) return toggleVariants(upstream, modelID) if (upstream) return toggleVariants(upstream, modelID)
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { reasoning: { enabled: false } }, settings: { reasoning: { enabled: false } },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { reasoning: { enabled: true } }, settings: { reasoning: { enabled: true } },
}, },
] ]
} }
if (npm === "@openrouter/ai-sdk-provider") if (npm === "@openrouter/ai-sdk-provider")
return [ return [
{ id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
{ id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
] ]
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return [ return [
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { settings: {
thinking: { type: "adaptive", display: "summarized" }, thinking: { type: "adaptive", display: "summarized" },
}, },
@ -333,11 +333,11 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } }, settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
}, },
] ]
@ -345,7 +345,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
const anthropic = modelID.includes("anthropic") const anthropic = modelID.includes("anthropic")
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { settings: {
additionalModelRequestFields: anthropic additionalModelRequestFields: anthropic
? { thinking: { type: "disabled" } } ? { thinking: { type: "disabled" } }
@ -353,7 +353,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
}, },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { settings: {
additionalModelRequestFields: anthropic additionalModelRequestFields: anthropic
? { thinking: { type: "adaptive", display: "summarized" } } ? { thinking: { type: "adaptive", display: "summarized" } }
@ -364,58 +364,58 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
} }
if (npm === "@ai-sdk/alibaba") if (npm === "@ai-sdk/alibaba")
return [ return [
{ id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, { id: Model.VariantID.make("none"), settings: { enableThinking: false } },
{ id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } }, { id: Model.VariantID.make("thinking"), settings: { enableThinking: true } },
] ]
if (npm === "@ai-sdk/cohere") if (npm === "@ai-sdk/cohere")
return [ return [
{ id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ id: ModelV2.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } }, { id: Model.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
] ]
if (npm === "@jerome-benoit/sap-ai-provider-v2") { if (npm === "@jerome-benoit/sap-ai-provider-v2") {
if (modelID.includes("gemini")) if (modelID.includes("gemini"))
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } }, settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } }, settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
}, },
] ]
if (modelID.includes("cohere")) if (modelID.includes("cohere"))
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { modelParams: { thinking: { type: "disabled" } } }, settings: { modelParams: { thinking: { type: "disabled" } } },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinking: { type: "enabled" } } }, settings: { modelParams: { thinking: { type: "enabled" } } },
}, },
] ]
if (modelID.includes("amazon--nova")) if (modelID.includes("amazon--nova"))
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } }, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } }, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
}, },
] ]
if (modelID.includes("anthropic")) if (modelID.includes("anthropic"))
return [ return [
{ {
id: ModelV2.VariantID.make("none"), id: Model.VariantID.make("none"),
settings: { settings: {
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } }, modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
}, },
}, },
{ {
id: ModelV2.VariantID.make("thinking"), id: Model.VariantID.make("thinking"),
settings: { settings: {
modelParams: { modelParams: {
additionalModelRequestFields: { additionalModelRequestFields: {
@ -429,7 +429,7 @@ function toggleVariants(npm: string, modelID: string): NonNullable<ModelV2.Info[
return [] return []
} }
function settingsForBudget(npm: string, modelID: string, budget: number): ProviderV2.Settings | undefined { function settingsForBudget(npm: string, modelID: string, budget: number): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } } if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return { thinking: { type: "enabled", budgetTokens: budget } } return { thinking: { type: "enabled", budgetTokens: budget } }
@ -480,24 +480,24 @@ function modeName(model: SourceModel, mode: string) {
} }
function modelInfo( function modelInfo(
providerID: ProviderV2.ID, providerID: Provider.ID,
id: ModelV2.ID, id: Model.ID,
model: SourceModel, model: SourceModel,
input: { input: {
readonly name?: string readonly name?: string
readonly cost?: ModelV2.Info["cost"] readonly cost?: Model.Info["cost"]
readonly request?: NonNullable<NonNullable<SourceModel["experimental"]>["modes"]>[string]["provider"] readonly request?: NonNullable<NonNullable<SourceModel["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelV2.Info["variants"]> readonly variants?: NonNullable<Model.Info["variants"]>
} = {}, } = {},
): ModelV2.Info { ): Model.Info {
return { return {
id, id,
modelID: ModelV2.ID.make(model.id), modelID: Model.ID.make(model.id),
providerID, providerID,
name: input.name ?? model.name, name: input.name ?? model.name,
compatibility: ModelV2.compatibility(model.interleaved), compatibility: Model.compatibility(model.interleaved),
family: model.family ? ModelV2.Family.make(model.family) : undefined, family: model.family ? Model.Family.make(model.family) : undefined,
package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined, package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined,
settings: model.provider?.api ? { baseURL: model.provider.api } : undefined, settings: model.provider?.api ? { baseURL: model.provider.api } : undefined,
capabilities: { capabilities: {
tools: model.tool_call, tools: model.tool_call,
@ -519,7 +519,7 @@ function modelInfo(
} }
} }
export const Event = ModelsDev.Event export { Event } from "@opencode-ai/schema/models-dev"
declare const OPENCODE_MODELS_DEV: Record<string, SourceProvider> | undefined declare const OPENCODE_MODELS_DEV: Record<string, SourceProvider> | undefined
@ -542,7 +542,7 @@ export const layer = (options?: Options) =>
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const app = yield* App.Metadata const app = yield* App.Metadata
const http = HttpClient.filterStatusOk( const http = HttpClient.filterStatusOk(
(yield* HttpClient.HttpClient).pipe( (yield* HttpClient.HttpClient).pipe(
@ -639,7 +639,7 @@ export const layer = (options?: Options) =>
if (!force && (yield* fresh())) return if (!force && (yield* fresh())) return
yield* fetchAndWrite() yield* fetchAndWrite()
yield* invalidate yield* invalidate
yield* events.publish(Event.Refreshed, {}) yield* bus.publish(ModelsDev.Event.Refreshed, {})
}), }),
).pipe( ).pipe(
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })),
@ -660,7 +660,7 @@ export function configured(options?: Options) {
return makeGlobalNode({ return makeGlobalNode({
service: Service, service: Service,
layer: layer(options), layer: layer(options),
deps: [FSUtil.node, EventV2.node, App.node, httpClient], deps: [FSUtil.node, Bus.node, App.node, httpClient],
}) })
} }

View file

@ -1,11 +1,11 @@
export * as PermissionV2 from "./permission" export * as Permission from "./permission"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { Location } from "./location" import { Location } from "./location"
import { AgentV2 } from "./agent" import { Agent } from "./agent"
import { SessionErrors } from "./session/error" import { SessionErrors } from "./session/error"
import { SessionSchema } from "./session/schema" import { SessionSchema } from "./session/schema"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
@ -41,32 +41,32 @@ export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({ export const AssertInput = Schema.Struct({
id: ID.pipe(Schema.optional), id: ID.pipe(Schema.optional),
...RequestFields, ...RequestFields,
agent: AgentV2.ID.pipe(Schema.optional), agent: Agent.ID.pipe(Schema.optional),
}).annotate({ identifier: "PermissionV2.AssertInput" }) }).annotate({ identifier: "Permission.AssertInput" })
export type AssertInput = typeof AssertInput.Type export type AssertInput = typeof AssertInput.Type
export const ReplyInput = Schema.Struct({ export const ReplyInput = Schema.Struct({
requestID: ID, requestID: ID,
reply: Reply, reply: Reply,
message: Schema.String.pipe(Schema.optional), message: Schema.String.pipe(Schema.optional),
}).annotate({ identifier: "PermissionV2.ReplyInput" }) }).annotate({ identifier: "Permission.ReplyInput" })
export type ReplyInput = typeof ReplyInput.Type export type ReplyInput = typeof ReplyInput.Type
export const AskResult = Schema.Struct({ export const AskResult = Schema.Struct({
id: ID, id: ID,
effect: Permission.Effect, effect: Permission.Effect,
}).annotate({ identifier: "PermissionV2.AskResult" }) }).annotate({ identifier: "Permission.AskResult" })
export type AskResult = typeof AskResult.Type export type AskResult = typeof AskResult.Type
export const Event = Permission.Event export { Event } from "@opencode-ai/schema/permission"
export class DeclinedError extends Schema.TaggedErrorClass<DeclinedError>()("PermissionV2.DeclinedError", {}) {} export class DeclinedError extends Schema.TaggedErrorClass<DeclinedError>()("Permission.DeclinedError", {}) {}
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", { export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("Permission.CorrectedError", {
feedback: Schema.String, feedback: Schema.String,
}) {} }) {}
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("PermissionV2.BlockedError", { export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("Permission.BlockedError", {
rules: Permission.Ruleset, rules: Permission.Ruleset,
permission: Schema.String, permission: Schema.String,
resources: Schema.Array(Schema.String), resources: Schema.Array(Schema.String),
@ -76,7 +76,7 @@ export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("Permi
} }
} }
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", { export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
requestID: ID, requestID: ID,
}) {} }) {}
@ -107,20 +107,20 @@ export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Request>> readonly list: () => Effect.Effect<ReadonlyArray<Request>>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
interface Pending { interface Pending {
readonly request: Request readonly request: Request
readonly agent?: AgentV2.ID readonly agent?: Agent.ID
readonly deferred: Deferred.Deferred<void, DeclinedError | CorrectedError> readonly deferred: Deferred.Deferred<void, DeclinedError | CorrectedError>
} }
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const location = yield* Location.Service const location = yield* Location.Service
const agents = yield* AgentV2.Service const agents = yield* Agent.Service
const sessions = yield* SessionStore.Service const sessions = yield* SessionStore.Service
const saved = yield* PermissionSaved.Service const saved = yield* PermissionSaved.Service
const pending = new Map<ID, Pending>() const pending = new Map<ID, Pending>()
@ -143,9 +143,9 @@ const layer = Layer.effect(
) )
}) })
const configured = Effect.fn("PermissionV2.configured")(function* ( const configured = Effect.fn("Permission.configured")(function* (
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
agentID?: AgentV2.ID, agentID?: Agent.ID,
) { ) {
const session = yield* sessions.get(sessionID) const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionErrors.NotFoundError({ sessionID }) if (!session) return yield* new SessionErrors.NotFoundError({ sessionID })
@ -182,7 +182,7 @@ const layer = Layer.effect(
} }
} }
const create = (request: Request, agent?: AgentV2.ID) => const create = (request: Request, agent?: Agent.ID) =>
Effect.uninterruptible( Effect.uninterruptible(
Effect.gen(function* () { Effect.gen(function* () {
const deferred = yield* Deferred.make<void, DeclinedError | CorrectedError>() const deferred = yield* Deferred.make<void, DeclinedError | CorrectedError>()
@ -190,21 +190,21 @@ const layer = Layer.effect(
if (pending.has(request.id)) if (pending.has(request.id))
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`)) return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
pending.set(request.id, item) pending.set(request.id, item)
yield* events yield* bus
.publish(Event.Asked, request) .publish(Permission.Event.Asked, request)
.pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id)))) .pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id))))
return item return item
}), }),
) )
const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) { const ask = Effect.fn("Permission.ask")(function* (input: AssertInput) {
const result = yield* evaluateInput(input) const result = yield* evaluateInput(input)
const value = request(input) const value = request(input)
if (result.effect === "ask") yield* create(value, input.agent) if (result.effect === "ask") yield* create(value, input.agent)
return { id: value.id, effect: result.effect } return { id: value.id, effect: result.effect }
}) })
const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) => const assert = Effect.fn("Permission.assert")((input: AssertInput) =>
Effect.uninterruptibleMask((restore) => Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () { Effect.gen(function* () {
const result = yield* evaluateInput(input) const result = yield* evaluateInput(input)
@ -223,7 +223,7 @@ const layer = Layer.effect(
// resurfaces as a typed failure at SessionModelRequest.executeTool. A decline // resurfaces as a typed failure at SessionModelRequest.executeTool. A decline
// WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn // WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn
// it into ToolFailure and the model continues. // it into ToolFailure and the model continues.
Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)), Effect.catchTag("Permission.DeclinedError", (error) => Effect.die(error)),
Effect.ensuring( Effect.ensuring(
Effect.sync(() => { Effect.sync(() => {
pending.delete(item.request.id) pending.delete(item.request.id)
@ -234,12 +234,12 @@ const layer = Layer.effect(
), ),
) )
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) => const reply = Effect.fn("Permission.reply")((input: ReplyInput) =>
Effect.uninterruptible( Effect.uninterruptible(
Effect.gen(function* () { Effect.gen(function* () {
const existing = pending.get(input.requestID) const existing = pending.get(input.requestID)
if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
yield* events.publish(Event.Replied, { yield* bus.publish(Permission.Event.Replied, {
sessionID: existing.request.sessionID, sessionID: existing.request.sessionID,
requestID: existing.request.id, requestID: existing.request.id,
reply: input.reply, reply: input.reply,
@ -253,7 +253,7 @@ const layer = Layer.effect(
pending.delete(input.requestID) pending.delete(input.requestID)
for (const [id, item] of pending) { for (const [id, item] of pending) {
if (item.request.sessionID !== existing.request.sessionID) continue if (item.request.sessionID !== existing.request.sessionID) continue
yield* events.publish(Event.Replied, { yield* bus.publish(Permission.Event.Replied, {
sessionID: item.request.sessionID, sessionID: item.request.sessionID,
requestID: item.request.id, requestID: item.request.id,
reply: "reject", reply: "reject",
@ -290,7 +290,7 @@ const layer = Layer.effect(
) )
) )
continue continue
yield* events.publish(Event.Replied, { yield* bus.publish(Permission.Event.Replied, {
sessionID: item.request.sessionID, sessionID: item.request.sessionID,
requestID: item.request.id, requestID: item.request.id,
reply: "always", reply: "always",
@ -302,15 +302,15 @@ const layer = Layer.effect(
), ),
) )
const list = Effect.fn("PermissionV2.list")(function* () { const list = Effect.fn("Permission.list")(function* () {
return Array.from(pending.values(), (item) => item.request) return Array.from(pending.values(), (item) => item.request)
}) })
const get = Effect.fn("PermissionV2.get")(function* (id: ID) { const get = Effect.fn("Permission.get")(function* (id: ID) {
return pending.get(id)?.request return pending.get(id)?.request
}) })
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionSchema.ID) { const forSession = Effect.fn("Permission.forSession")(function* (sessionID: SessionSchema.ID) {
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
}) })
@ -321,5 +321,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({ export const node = makeLocationNode({
service: Service, service: Service,
layer, layer,
deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node], deps: [Bus.node, Location.node, Agent.node, SessionStore.node, PermissionSaved.node],
}) })

View file

@ -4,7 +4,7 @@ import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database" import { Database } from "../database/database"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { ProjectV2 } from "../project" import { Project } from "../project"
import { PermissionTable } from "./sql" import { PermissionTable } from "./sql"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved" import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
@ -15,12 +15,12 @@ export const Info = PermissionSaved.Info
export type Info = typeof Info.Type export type Info = typeof Info.Type
export const ListInput = Schema.Struct({ export const ListInput = Schema.Struct({
projectID: ProjectV2.ID.pipe(Schema.optional), projectID: Project.ID.pipe(Schema.optional),
}).annotate({ identifier: "PermissionSaved.ListInput" }) }).annotate({ identifier: "PermissionSaved.ListInput" })
export type ListInput = typeof ListInput.Type export type ListInput = typeof ListInput.Type
export const AddInput = Schema.Struct({ export const AddInput = Schema.Struct({
projectID: ProjectV2.ID, projectID: Project.ID,
action: Schema.String, action: Schema.String,
resources: Schema.Array(Schema.String), resources: Schema.Array(Schema.String),
}).annotate({ identifier: "PermissionSaved.AddInput" }) }).annotate({ identifier: "PermissionSaved.AddInput" })
@ -32,7 +32,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void> readonly remove: (id: ID) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PermissionSaved") {} export class Service extends Context.Service<Service, Interface>()("@opencode/PermissionSaved") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -1,6 +1,6 @@
import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../database/schema.sql" import { Timestamps } from "../database/schema.sql"
import { ProjectV2 } from "../project" import { Project } from "../project"
import { ProjectTable } from "../project/sql" import { ProjectTable } from "../project/sql"
import type { PermissionSaved } from "./saved" import type { PermissionSaved } from "./saved"
@ -9,7 +9,7 @@ export const PermissionTable = sqliteTable(
{ {
id: text().$type<PermissionSaved.ID>().primaryKey(), id: text().$type<PermissionSaved.ID>().primaryKey(),
project_id: text() project_id: text()
.$type<ProjectV2.ID>() .$type<Project.ID>()
.notNull() .notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }), .references(() => ProjectTable.id, { onDelete: "cascade" }),
action: text().notNull(), action: text().notNull(),

View file

@ -1,46 +1,43 @@
export * as PluginV2 from "./plugin" export * as Plugin from "./plugin"
export { Event, ID, Info } from "@opencode-ai/schema/plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" import { Plugin } from "@opencode-ai/schema/plugin"
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { App } from "./app" import { App } from "./app"
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect" import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
import { AgentV2 } from "./agent" import { Agent } from "./agent"
import { AISDK } from "./aisdk" import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
import { CommandV2 } from "./command" import { Command } from "./command"
import { EventV2 } from "./event" import { Bus } from "./bus"
import { Integration } from "./integration" import { Integration } from "./integration"
import { Location } from "./location" import { Location } from "./location"
import { PluginHost } from "./plugin/host" import { PluginHost } from "./plugin/host"
import { PluginRuntime } from "./plugin/runtime" import { PluginRuntime } from "./plugin/runtime"
import { WebSearch } from "./websearch" import { WebSearch } from "./websearch"
import { Reference } from "./reference" import { Reference } from "./reference"
import { SkillV2 } from "./skill" import { Skill } from "./skill"
import { State } from "./state" import { State } from "./state"
import { ToolRegistry } from "./tool/registry" import { Tool } from "./tool"
import { ToolHooks } from "./tool/hooks"
import { PluginHooks } from "./plugin/hooks" import { PluginHooks } from "./plugin/hooks"
export interface Interface { export interface Interface {
readonly activate: (plugins: readonly Versioned[]) => Effect.Effect<void> readonly activate: (plugins: readonly Versioned[]) => Effect.Effect<void>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Plugin.Info[]>
} }
export interface Versioned extends Plugin { export type Versioned = import("@opencode-ai/plugin/effect/plugin").Plugin & { readonly version: string }
readonly version: string
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const bus = yield* Bus.Service
const scope = yield* Scope.make() const scope = yield* Scope.make()
const active = new Map<typeof ID.Type, { readonly plugin: Versioned; readonly scope: Scope.Closeable }>() const active = new Map<Plugin.ID, { readonly plugin: Versioned; readonly scope: Scope.Closeable }>()
const lock = Semaphore.makeUnsafe(1) const lock = Semaphore.makeUnsafe(1)
let host: Parameters<Plugin["effect"]>[0] let host: Parameters<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>[0]
const load = Effect.fnUntraced(function* (plugin: Versioned) { const load = Effect.fnUntraced(function* (plugin: Versioned) {
const child = yield* Scope.fork(scope) const child = yield* Scope.fork(scope)
@ -49,7 +46,7 @@ const layer = Layer.effect(
inherit, inherit,
Effect.updateContext((_context: Context.Context<never>) => Context.make(Scope.Scope, child)), Effect.updateContext((_context: Context.Context<never>) => Context.make(Scope.Scope, child)),
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }), Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }),
Effect.andThen(events.publish(Event.Added, { id: ID.make(plugin.id) })), Effect.andThen(bus.publish(Plugin.Event.Added, { id: Plugin.ID.make(plugin.id) })),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
Effect.exit, Effect.exit,
) )
@ -62,8 +59,8 @@ const layer = Layer.effect(
}) })
const activate = Effect.fn("Plugin.activate")(function* (plugins: readonly Versioned[]) { const activate = Effect.fn("Plugin.activate")(function* (plugins: readonly Versioned[]) {
const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) })) const definitions = plugins.map((plugin) => ({ ...plugin, id: Plugin.ID.make(plugin.id) }))
const ids = new Set<typeof ID.Type>() const ids = new Set<Plugin.ID>()
for (const definition of definitions) { for (const definition of definitions) {
if (ids.has(definition.id)) yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`)) if (ids.has(definition.id)) yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`))
ids.add(definition.id) ids.add(definition.id)
@ -118,7 +115,7 @@ const layer = Layer.effect(
}) })
}), }),
) )
yield* events.publish(Event.Updated, {}) yield* bus.publish(Plugin.Event.Updated, {})
}), }),
) )
}) })
@ -145,18 +142,17 @@ export const node = makeLocationNode({
service: Service, service: Service,
layer, layer,
deps: [ deps: [
EventV2.node, Bus.node,
App.node, App.node,
AgentV2.node, Agent.node,
AISDK.node, AISDK.node,
Catalog.node, Catalog.node,
CommandV2.node, Command.node,
Integration.node, Integration.node,
Location.node, Location.node,
Reference.node, Reference.node,
SkillV2.node, Skill.node,
ToolRegistry.toolsNode, Tool.node,
ToolHooks.node,
PluginHooks.node, PluginHooks.node,
PluginRuntime.node, PluginRuntime.node,
WebSearch.node, WebSearch.node,

View file

@ -1,12 +1,12 @@
export * as AgentPlugin from "./agent" export * as AgentPlugin from "./agent"
import path from "path" import path from "path"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect" import { Effect } from "effect"
import { AgentV2 } from "../agent" import { Agent } from "../agent"
import { Global } from "@opencode-ai/util/global" import { Global } from "@opencode-ai/util/global"
import { Location } from "../location" import { Location } from "../location"
import { PermissionV2 } from "../permission" import { Permission } from "../permission"
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`. // Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
// Whitelisted so agents can read a command's full captured output without an external-directory prompt. // Whitelisted so agents can read a command's full captured output without an external-directory prompt.
@ -105,13 +105,13 @@ export const Plugin = define({
const location = yield* Location.Service const location = yield* Location.Service
const worktree = location.directory const worktree = location.directory
const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")] const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")]
const readonlyExternalDirectory: PermissionV2.Ruleset = [ const readonlyExternalDirectory: Permission.Ruleset = [
{ action: "external_directory", resource: "*", effect: "ask" }, { action: "external_directory", resource: "*", effect: "ask" },
...whitelistedDirs.map( ...whitelistedDirs.map(
(resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }), (resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }),
), ),
] ]
const defaults: PermissionV2.Ruleset = [ const defaults: Permission.Ruleset = [
{ action: "*", resource: "*", effect: "allow" }, { action: "*", resource: "*", effect: "allow" },
...readonlyExternalDirectory, ...readonlyExternalDirectory,
{ action: "question", resource: "*", effect: "deny" }, { action: "question", resource: "*", effect: "deny" },
@ -124,24 +124,24 @@ export const Plugin = define({
] ]
yield* ctx.agent.transform((draft) => { yield* ctx.agent.transform((draft) => {
draft.update(AgentV2.defaultID, (item) => { draft.update(Agent.defaultID, (item) => {
item.name = AgentV2.Name.make("Build") item.name = Agent.Name.make("Build")
item.description = "The default agent. Executes tools based on configured permissions." item.description = "The default agent. Executes tools based on configured permissions."
item.mode = "primary" item.mode = "primary"
item.permissions.push( item.permissions.push(
...PermissionV2.merge(defaults, [ ...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" }, { action: "question", resource: "*", effect: "allow" },
{ action: "plan_enter", resource: "*", effect: "allow" }, { action: "plan_enter", resource: "*", effect: "allow" },
]), ]),
) )
}) })
draft.update(AgentV2.ID.make("plan"), (item) => { draft.update(Agent.ID.make("plan"), (item) => {
item.name = AgentV2.Name.make("Plan") item.name = Agent.Name.make("Plan")
item.description = "Plan mode. Disallows all edit tools." item.description = "Plan mode. Disallows all edit tools."
item.mode = "primary" item.mode = "primary"
item.permissions.push( item.permissions.push(
...PermissionV2.merge(defaults, [ ...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" }, { action: "question", resource: "*", effect: "allow" },
{ action: "plan_exit", resource: "*", effect: "allow" }, { action: "plan_exit", resource: "*", effect: "allow" },
{ action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" },
@ -156,22 +156,22 @@ export const Plugin = define({
) )
}) })
draft.update(AgentV2.ID.make("general"), (item) => { draft.update(Agent.ID.make("general"), (item) => {
item.name = AgentV2.Name.make("General") item.name = Agent.Name.make("General")
item.description = item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
item.mode = "subagent" item.mode = "subagent"
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
}) })
draft.update(AgentV2.ID.make("explore"), (item) => { draft.update(Agent.ID.make("explore"), (item) => {
item.name = AgentV2.Name.make("Explore") item.name = Agent.Name.make("Explore")
item.description = item.description =
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
item.system = PROMPT_EXPLORE item.system = PROMPT_EXPLORE
item.mode = "subagent" item.mode = "subagent"
item.permissions.push( item.permissions.push(
...PermissionV2.merge( ...Permission.merge(
defaults, defaults,
[ [
{ action: "*", resource: "*", effect: "deny" }, { action: "*", resource: "*", effect: "deny" },
@ -187,28 +187,28 @@ export const Plugin = define({
) )
}) })
draft.update(AgentV2.ID.make("compaction"), (item) => { draft.update(Agent.ID.make("compaction"), (item) => {
item.name = AgentV2.Name.make("Compaction") item.name = Agent.Name.make("Compaction")
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_COMPACTION item.system = PROMPT_COMPACTION
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
}) })
draft.update(AgentV2.ID.make("title"), (item) => { draft.update(Agent.ID.make("title"), (item) => {
item.name = AgentV2.Name.make("Title") item.name = Agent.Name.make("Title")
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_TITLE item.system = PROMPT_TITLE
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
}) })
draft.update(AgentV2.ID.make("summary"), (item) => { draft.update(Agent.ID.make("summary"), (item) => {
item.name = AgentV2.Name.make("Summary") item.name = Agent.Name.make("Summary")
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_SUMMARY item.system = PROMPT_SUMMARY
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
}) })
}) })
}), }),

View file

@ -1,6 +1,6 @@
export * as CommandPlugin from "./command" export * as CommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect" import { Effect } from "effect"
import { Location } from "../location" import { Location } from "../location"
import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_INITIALIZE from "./command/initialize.txt"

View file

@ -1,8 +1,8 @@
export * as PluginHooks from "./hooks" export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk" import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool" import type { ToolHooks } from "@opencode-ai/plugin/effect/tool"
import { Context, Effect, Layer, Scope } from "effect" import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { State } from "../state" import { State } from "../state"
@ -28,7 +28,7 @@ export interface Interface {
) => Effect.Effect<Domains[Domain][Name]> ) => Effect.Effect<Domains[Domain][Name]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {} export class Service extends Context.Service<Service, Interface>()("@opencode/PluginHooks") {}
const layer = Layer.effect( const layer = Layer.effect(
Service, Service,

View file

@ -1,48 +1,44 @@
export * as PluginHost from "./host" export * as PluginHost from "./host"
import { Plugin } from "@opencode-ai/plugin/v2/effect" import { Plugin } from "@opencode-ai/plugin/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types"
import { EventManifest } from "@opencode-ai/schema/event-manifest" import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { App } from "../app" import { App } from "../app"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent" import { Agent } from "../agent"
import { AISDK } from "../aisdk" import { AISDK } from "../aisdk"
import { Catalog } from "../catalog" import { Catalog } from "../catalog"
import { CommandV2 } from "../command" import { Command } from "../command"
import { Credential } from "../credential" import { Credential } from "../credential"
import { EventV2 } from "../event" import { Bus } from "../bus"
import { Integration } from "../integration" import { Integration } from "../integration"
import { Location } from "../location" import { Location } from "../location"
import { ModelV2 } from "../model" import { Model } from "../model"
import type { PluginV2 } from "../plugin"
import { PluginRuntime } from "./runtime" import { PluginRuntime } from "./runtime"
import { ProviderV2 } from "../provider" import { Provider } from "../provider"
import { Reference } from "../reference" import { Reference } from "../reference"
import { AbsolutePath, type DeepMutable } from "../schema" import { AbsolutePath, type DeepMutable } from "../schema"
import { SkillV2 } from "../skill" import { Skill } from "../skill"
import { Tool } from "../tool/tool" import { Tool } from "../tool"
import { Tools } from "../tool/tools" import { Workspace } from "../workspace"
import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
import { WebSearch } from "../websearch" import { WebSearch } from "../websearch"
import { PluginHooks } from "./hooks" import { PluginHooks } from "./hooks"
const mutable = <T>(value: T) => value as DeepMutable<T> const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../plugin").Interface) {
const app = yield* App.Metadata const app = yield* App.Metadata
const agents = yield* AgentV2.Service const agents = yield* Agent.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service const commands = yield* Command.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const integration = yield* Integration.Service const integration = yield* Integration.Service
const location = yield* Location.Service const location = yield* Location.Service
const reference = yield* Reference.Service const reference = yield* Reference.Service
const skill = yield* SkillV2.Service const skill = yield* Skill.Service
const tools = yield* Tools.Service const tools = yield* Tool.Service
const websearch = yield* WebSearch.Service const websearch = yield* WebSearch.Service
const toolHooks = yield* ToolHooks.Service
const hooks = yield* PluginHooks.Service const hooks = yield* PluginHooks.Service
const runtime = yield* PluginRuntime.Service const runtime = yield* PluginRuntime.Service
const locationInfo = () => const locationInfo = () =>
@ -51,7 +47,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID, workspaceID: location.workspaceID,
project: location.project, project: location.project,
}) })
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) => const locationRef = (input?: {
readonly location?: { readonly directory?: string; readonly workspace?: string }
}) =>
input?.location === undefined input?.location === undefined
? undefined ? undefined
: Location.Ref.make({ : Location.Ref.make({
@ -59,7 +57,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: workspaceID:
input.location.workspace === undefined input.location.workspace === undefined
? location.workspaceID ? location.workspaceID
: WorkspaceV2.ID.make(input.location.workspace), : Workspace.ID.make(input.location.workspace),
}) })
const isCurrentLocation = (ref: Location.Ref) => const isCurrentLocation = (ref: Location.Ref) =>
ref.directory === location.directory && ref.workspaceID === location.workspaceID ref.directory === location.directory && ref.workspaceID === location.workspaceID
@ -70,7 +68,22 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
app, app,
options: {}, options: {},
agent: { agent: {
get: (id) => agents.get(AgentV2.ID.make(id)), get: (input) => {
const ref = locationRef(input)
const output =
ref && !isCurrentLocation(ref)
? runtime.location.agent
.list(ref)
.pipe(Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID) })))
: response(agents.get(input.agentID))
return output.pipe(
Effect.flatMap((result) =>
result.data
? Effect.succeed({ ...result, data: result.data })
: Effect.fail(new Error(`Agent not found: ${input.agentID}`)),
),
)
},
list: (input) => { list: (input) => {
const ref = locationRef(input) const ref = locationRef(input)
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref) if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
@ -81,10 +94,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
agents.transform((draft) => { agents.transform((draft) => {
callback({ callback({
list: () => mutable(draft.list()), list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(AgentV2.ID.make(id))), get: (id) => mutable(draft.get(Agent.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update), update: (id, update) => draft.update(Agent.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)), remove: (id) => draft.remove(Agent.ID.make(id)),
}) })
}), }),
}, },
@ -121,7 +134,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
list: () => response(catalog.provider.available()), list: () => response(catalog.provider.available()),
get: (input) => get: (input) =>
catalog.provider catalog.provider
.get(ProviderV2.ID.make(input.providerID)) .get(Provider.ID.make(input.providerID))
.pipe( .pipe(
Effect.flatMap((provider) => Effect.flatMap((provider) =>
provider === undefined provider === undefined
@ -131,7 +144,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
), ),
}, },
model: { model: {
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: () => response(catalog.model.available()), list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()), default: () => response(catalog.model.default()),
}, },
@ -141,21 +153,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
callback({ callback({
provider: { provider: {
list: () => mutable(draft.provider.list()), list: () => mutable(draft.provider.list()),
get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))), get: (id) => mutable(draft.provider.get(Provider.ID.make(id))),
update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update), update: (id, update) => draft.provider.update(Provider.ID.make(id), update),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), remove: (id) => draft.provider.remove(Provider.ID.make(id)),
}, },
model: { model: {
get: (providerID, modelID) => get: (providerID, modelID) =>
mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))), mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))),
update: (providerID, modelID, update) => update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update), draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update),
remove: (providerID, modelID) => remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
default: { default: {
get: draft.model.default.get, get: draft.model.default.get,
set: (providerID, modelID) => set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
}, },
}, },
}) })
@ -170,7 +182,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
}, },
event: { event: {
subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)), subscribe: () => bus.subscribe().pipe(Stream.filter(EventManifest.isServer)),
}, },
integration: { integration: {
list: () => response(integration.list()), list: () => response(integration.list()),
@ -279,88 +291,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
transform: (callback) => transform: (callback) =>
skill.transform((draft) => { skill.transform((draft) => {
callback({ callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), source: (source) => draft.source(Schema.decodeUnknownSync(Skill.Source)(source)),
list: draft.list, list: draft.list,
}) })
}), }),
}, },
tool: { tool: {
transform: (callback) => transform: (callback) =>
Effect.gen(function* () { tools
const registrations: Array<{ .transform((draft) =>
readonly name: string
readonly tool: Tool.Any
readonly options?: Tool.RegisterOptions
}> = []
yield* Effect.sync(() =>
callback({ callback({
add: (name, tool, options) => { add: (tool) => draft.add(tool),
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
}), }),
) )
yield* tools .pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
.registerBatch( hook: (name, callback) => hooks.register("tool", name, callback),
registrations.map((registration) => ({
tools: { [registration.name]: registration.tool },
...(registration.options === undefined ? {} : { options: registration.options }),
})),
)
.pipe(Effect.orDie)
return { dispose: Effect.void }
}),
hook: (name, callback) => {
if (name === "execute.before") {
return toolHooks.hook.before((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
)
})
}
return toolHooks.hook.after((event) => {
// Decode first so plugin mutations cannot alias the canonical outcome.
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event),
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => {
const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output)
if (decoded._tag === "None")
return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool })
if (decoded.value.status !== event.status)
return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool })
return Effect.sync(() => {
if (event.status === "completed" && decoded.value.status === "completed") {
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
return
}
if (event.status === "error" && decoded.value.status === "error") {
event.error = decoded.value.error
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
}
})
}),
)
})
},
}, },
websearch: { websearch: {
providers: () => response(websearch.providers()), providers: () => response(websearch.providers()),

View file

@ -1,11 +1,11 @@
export * as PluginInternal from "./internal" export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Scope } from "effect" import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http" import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent" import { Agent } from "../agent"
import { Catalog } from "../catalog" import { Catalog } from "../catalog"
import { CommandV2 } from "../command" import { Command } from "../command"
import { Config } from "../config" import { Config } from "../config"
import { ConfigAgentPlugin } from "../config/plugin/agent" import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command" import { ConfigCommandPlugin } from "../config/plugin/command"
@ -14,7 +14,7 @@ import { ConfigPolicyPlugin } from "../config/plugin/policy"
import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigReferencePlugin } from "../config/plugin/reference"
import { ConfigSkillPlugin } from "../config/plugin/skill" import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigWebSearchPlugin } from "../config/plugin/websearch" import { ConfigWebSearchPlugin } from "../config/plugin/websearch"
import { EventV2 } from "../event" import { Bus } from "../bus"
import { FileMutation } from "../file-mutation" import { FileMutation } from "../file-mutation"
import { Form } from "../form" import { Form } from "../form"
import { FileSystem } from "../filesystem" import { FileSystem } from "../filesystem"
@ -27,28 +27,28 @@ import { Location } from "../location"
import { LocationMutation } from "../location-mutation" import { LocationMutation } from "../location-mutation"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
import { Npm } from "@opencode-ai/util/npm" import { Npm } from "@opencode-ai/util/npm"
import { PermissionV2 } from "../permission" import { Permission } from "../permission"
import { Reference } from "../reference" import { Reference } from "../reference"
import { WebSearch } from "../websearch" import { WebSearch } from "../websearch"
import { Ripgrep } from "../ripgrep" import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions" import { SessionInstructions } from "../session/instructions"
import { Shell } from "../shell" import { Shell } from "../shell"
import { SkillV2 } from "../skill" import { Skill } from "../skill"
import { PatchTool } from "../tool/patch" import { PatchTool } from "../tool/plugin/patch"
import { EditTool } from "../tool/edit" import { EditTool } from "../tool/plugin/edit"
import { GlobTool } from "../tool/glob" import { GlobTool } from "../tool/plugin/glob"
import { GrepTool } from "../tool/grep" import { GrepTool } from "../tool/plugin/grep"
import { QuestionTool } from "../tool/question" import { QuestionTool } from "../tool/plugin/question"
import { ReadToolFileSystem } from "../tool/read-filesystem" import { ReadToolFileSystem } from "../tool/read-filesystem"
import { ReadTool } from "../tool/read" import { ReadTool } from "../tool/plugin/read"
import { ShellTool } from "../tool/shell" import { ShellTool } from "../tool/plugin/shell"
import { SkillTool } from "../tool/skill" import { SkillTool } from "../tool/plugin/skill"
import { SubagentTool } from "../tool/subagent" import { SubagentTool } from "../tool/plugin/subagent"
import { Tools } from "../tool/tools" import { Tool } from "../tool"
import { WebFetchTool } from "../tool/webfetch" import { WebFetchTool } from "../tool/plugin/webfetch"
import { WebSearchTool } from "../tool/websearch" import { WebSearchTool } from "../tool/plugin/websearch"
import { WellKnown } from "../wellknown" import { WellKnown } from "../wellknown"
import { WriteTool } from "../tool/write" import { WriteTool } from "../tool/plugin/write"
import { AgentPlugin } from "./agent" import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command" import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev" import { ModelsDevPlugin } from "./models-dev"
@ -62,11 +62,11 @@ import { WarmingPlugin } from "./warming"
import { WellKnownPlugin } from "../wellknown/plugin" import { WellKnownPlugin } from "../wellknown/plugin"
const services = Effect.fn("PluginInternal.services")(function* () { const services = Effect.fn("PluginInternal.services")(function* () {
const agent = yield* AgentV2.Service const agent = yield* Agent.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const command = yield* CommandV2.Service const command = yield* Command.Service
const config = yield* Config.Service const config = yield* Config.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const mutation = yield* FileMutation.Service const mutation = yield* FileMutation.Service
const filesystem = yield* FileSystem.Service const filesystem = yield* FileSystem.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
@ -79,7 +79,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const locationMutation = yield* LocationMutation.Service const locationMutation = yield* LocationMutation.Service
const models = yield* ModelsDev.Service const models = yield* ModelsDev.Service
const npm = yield* Npm.Service const npm = yield* Npm.Service
const permission = yield* PermissionV2.Service const permission = yield* Permission.Service
const runtime = yield* PluginRuntime.Service const runtime = yield* PluginRuntime.Service
const form = yield* Form.Service const form = yield* Form.Service
const read = yield* ReadToolFileSystem.Service const read = yield* ReadToolFileSystem.Service
@ -88,15 +88,15 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const ripgrep = yield* Ripgrep.Service const ripgrep = yield* Ripgrep.Service
const instructions = yield* SessionInstructions.Service const instructions = yield* SessionInstructions.Service
const shell = yield* Shell.Service const shell = yield* Shell.Service
const skill = yield* SkillV2.Service const skill = yield* Skill.Service
const tools = yield* Tools.Service const tools = yield* Tool.Service
const wellknown = yield* WellKnown.Service const wellknown = yield* WellKnown.Service
return Context.mergeAll( return Context.mergeAll(
Context.make(AgentV2.Service, agent), Context.make(Agent.Service, agent),
Context.make(Catalog.Service, catalog), Context.make(Catalog.Service, catalog),
Context.make(CommandV2.Service, command), Context.make(Command.Service, command),
Context.make(Config.Service, config), Context.make(Config.Service, config),
Context.make(EventV2.Service, events), Context.make(Bus.Service, bus),
Context.make(FileMutation.Service, mutation), Context.make(FileMutation.Service, mutation),
Context.make(FileSystem.Service, filesystem), Context.make(FileSystem.Service, filesystem),
Context.make(FSUtil.Service, fs), Context.make(FSUtil.Service, fs),
@ -109,7 +109,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(LocationMutation.Service, locationMutation), Context.make(LocationMutation.Service, locationMutation),
Context.make(ModelsDev.Service, models), Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm), Context.make(Npm.Service, npm),
Context.make(PermissionV2.Service, permission), Context.make(Permission.Service, permission),
Context.make(PluginRuntime.Service, runtime), Context.make(PluginRuntime.Service, runtime),
Context.make(Form.Service, form), Context.make(Form.Service, form),
Context.make(ReadToolFileSystem.Service, read), Context.make(ReadToolFileSystem.Service, read),
@ -118,8 +118,8 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Ripgrep.Service, ripgrep), Context.make(Ripgrep.Service, ripgrep),
Context.make(SessionInstructions.Service, instructions), Context.make(SessionInstructions.Service, instructions),
Context.make(Shell.Service, shell), Context.make(Shell.Service, shell),
Context.make(SkillV2.Service, skill), Context.make(Skill.Service, skill),
Context.make(Tools.Service, tools), Context.make(Tool.Service, tools),
Context.make(WellKnown.Service, wellknown), Context.make(WellKnown.Service, wellknown),
) )
}) })

View file

@ -1,13 +1,14 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import { Integration } from "@opencode-ai/schema/integration"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { EventV2 } from "../event" import { Bus } from "../bus"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
export const ModelsDevPlugin = define({ export const ModelsDevPlugin = define({
id: "opencode.models-dev", id: "opencode.models-dev",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service const bus = yield* Bus.Service
const loaded = { data: structuredClone(yield* modelsDev.get()) } const loaded = { data: structuredClone(yield* modelsDev.get()) }
yield* ctx.integration.transform((integrations) => { yield* ctx.integration.transform((integrations) => {
for (const provider of loaded.data) { for (const provider of loaded.data) {
@ -28,14 +29,14 @@ export const ModelsDevPlugin = define({
for (const provider of loaded.data) { for (const provider of loaded.data) {
catalog.provider.update(provider.info.id, (draft) => { catalog.provider.update(provider.info.id, (draft) => {
Object.assign(draft, provider.info) Object.assign(draft, provider.info)
draft.integrationID = provider.info.id draft.integrationID = Integration.ID.make(provider.info.id)
}) })
for (const model of provider.models) { for (const model of provider.models) {
catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model)) catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model))
} }
} }
}) })
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() => Stream.runForEach(() =>
modelsDev.get().pipe( modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))), Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))),

View file

@ -1,8 +1,8 @@
export * as PluginPromise from "./promise" export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin" import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool" import type { Info } from "@opencode-ai/plugin/promise/tool"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location" import { Location } from "@opencode-ai/schema/location"
@ -14,7 +14,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace" import { Workspace } from "@opencode-ai/schema/workspace"
import { WebSearch } from "@opencode-ai/schema/websearch" import { WebSearch } from "@opencode-ai/schema/websearch"
import { DateTime, Effect, Scope, Stream } from "effect" import { DateTime, Effect, Scope, Stream } from "effect"
import { Tool } from "../tool/tool" import { Tool } from "../tool"
type HostRegistration = { readonly dispose: Effect.Effect<void> } type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> } type Registration = { readonly dispose: () => Promise<void> }
@ -23,7 +23,7 @@ type JsonValue = null | boolean | number | string | Array<JsonValue> | { [key: s
/** /**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only * Adapts a Promise plugin into an Effect plugin so the existing Effect-only
* loader (`PluginV2` / `PluginSupervisor`) can run it unchanged. * loader (`Plugin` / `PluginSupervisor`) can run it unchanged.
* *
* Hook registrations created during the async `setup` attach to the plugin's * Hook registrations created during the async `setup` attach to the plugin's
* scope, so unloading the plugin disposes them. The captured fiber context * scope, so unloading the plugin disposes them. The captured fiber context
@ -61,7 +61,7 @@ export function fromPromise(plugin: Plugin) {
app: host.app, app: host.app,
options: host.options, options: host.options,
agent: { agent: {
get: (id) => run(host.agent.get(id)), get: (input) => run(host.agent.get({ ...input, agentID: Agent.ID.make(input.agentID) })),
list: (input) => run(host.agent.list(input)), list: (input) => run(host.agent.list(input)),
transform: transform(host.agent), transform: transform(host.agent),
reload: () => run(host.agent.reload()), reload: () => run(host.agent.reload()),
@ -77,7 +77,6 @@ export function fromPromise(plugin: Plugin) {
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })), run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
}, },
model: { model: {
get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)),
list: (input) => run(host.catalog.model.list(input)), list: (input) => run(host.catalog.model.list(input)),
default: (input) => default: (input) =>
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })), run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
@ -165,7 +164,44 @@ export function fromPromise(plugin: Plugin) {
}), }),
), ),
}, },
transform: transform(host.integration), transform: (callback) =>
register(
host.integration.transform((draft) =>
callback({
list: draft.list,
get: draft.get,
update: draft.update,
remove: draft.remove,
method: {
list: draft.method.list,
update: (input) => {
if (!("authorize" in input)) return draft.method.update(input)
const refresh = input.refresh
draft.method.update({
...input,
authorize: (inputs) =>
Effect.promise(() => input.authorize(inputs)).pipe(
Effect.map((authorization) =>
authorization.mode === "auto"
? {
...authorization,
callback: Effect.promise(() => authorization.callback),
}
: {
...authorization,
callback: (code) => Effect.promise(() => authorization.callback(code)),
},
),
),
refresh:
refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)),
})
},
remove: draft.method.remove,
},
}),
),
),
reload: () => run(host.integration.reload()), reload: () => run(host.integration.reload()),
connection: { connection: {
active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)), active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)),
@ -190,8 +226,11 @@ export function fromPromise(plugin: Plugin) {
register( register(
host.tool.transform((draft) => host.tool.transform((draft) =>
callback({ callback({
add: (name: string, tool: Any, options?: RegisterOptions) => add: (tool: Info) =>
draft.add(name, fromPromiseTool(tool), options), draft.add({
...tool,
execute: (input, context) => executePromiseTool(tool, input, context),
}),
}), }),
), ),
), ),
@ -333,15 +372,10 @@ function wireEvent(value: unknown): unknown {
return wire(value) return wire(value)
} }
function fromPromiseTool(tool: Any): Tool.Any { const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
return { Effect.promise(() =>
...tool, tool.execute(input, {
execute: (input, context) => ...context,
Effect.promise(() => progress: (update) => Effect.runPromise(context.progress(update)),
tool.execute(input, { }),
...context, )
progress: (update) => Effect.runPromise(context.progress(update)),
}),
),
}
}

View file

@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/effect/plugin"
export const AlibabaPlugin = define({ export const AlibabaPlugin = define({
id: "opencode.provider.alibaba", id: "opencode.provider.alibaba",

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