Compare commits

..

19 Commits

Author SHA1 Message Date
Kit Langton 0574107e18 tui: active tabs now show more of their title before truncating
The selected tab in the titlebar expands from 224px to 288px so users can read more of the session or draft name before it gets cut off. Inactive tabs stay compact at 224px to keep the tab strip dense.
2026-07-30 20:46:52 -04:00
Kit Langton 6f0fa2d6c6 core: remove unused ownerHeld bookkeeping in service restarter
The ownerHeld flag was assigned but never read, making the service restart retry logic harder to follow. Removing it clarifies that the spawn delay only doubles when a previous contender finished cleanly.
2026-07-30 20:46:46 -04:00
Kit Langton 363905dcdd fix(tui): share runtime with external plugins 2026-07-28 21:00:49 -04:00
Kit Langton d71bc0055a fix(core): refresh reused session recency 2026-07-28 21:00:43 -04:00
Kit Langton 4f235fc59b feat(voice): harden controller runtime 2026-07-28 21:00:35 -04:00
Kit Langton 1d772f4896 feat(voice): add audio-reactive TUI animations 2026-07-27 16:08:14 -04:00
Kit Langton 113a117cef feat(voice): curate project session and permission tools 2026-07-27 15:46:48 -04:00
Kit Langton c103ba6056 feat(voice): show readable tool lifecycle diagnostics 2026-07-27 15:11:48 -04:00
Kit Langton 38f03c9d8b fix(voice): default coding sessions to gpt-5.6-sol medium 2026-07-27 15:08:03 -04:00
Kit Langton e5ac4ca560 fix(voice): run reactive opentui and harden shutdown 2026-07-27 14:53:27 -04:00
Kit Langton 5f0487fa87 feat(voice): opentui interface, voice switching, ordered transcripts 2026-07-27 14:23:40 -04:00
Kit Langton 00557f9078 fix(voice): rebuild audio engines on route change, cleaner output 2026-07-27 14:09:28 -04:00
Kit Langton 5ac1089c5c fix(voice): make echo cancellation opt-in, self-healing audio helper 2026-07-27 14:02:49 -04:00
Kit Langton d1843f49ac feat(voice): echo-cancelled full duplex via Apple voice processing 2026-07-27 13:49:23 -04:00
Kit Langton aac66d384e fix(voice): stop server VAD from cancelling half-duplex playback 2026-07-27 13:36:19 -04:00
Kit Langton 83108c1b5d feat(voice): --session flag to pin an existing session 2026-07-27 11:52:34 -04:00
Kit Langton e7000161f7 fix(voice): gate mic against speaker echo 2026-07-26 12:33:09 -04:00
Kit Langton 66200eee34 fix(voice): password flag and friendly connect error 2026-07-26 12:00:17 -04:00
Kit Langton 699477755f feat(voice): realtime voice control spike 2026-07-26 11:09:28 -04:00
535 changed files with 18691 additions and 9522 deletions
+35 -20
View File
@@ -184,7 +184,7 @@
"effect": "catalog:",
},
"peerDependencies": {
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.98",
},
"optionalPeers": [
"effect",
@@ -376,7 +376,6 @@
"@openrouter/ai-sdk-provider": "2.9.0",
"@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4",
"@standard-schema/spec": "catalog:",
"ai-gateway-provider": "3.1.2",
"bun-pty": "0.4.8",
"diff": "catalog:",
@@ -547,7 +546,7 @@
"name": "@opencode-ai/http-recorder",
"version": "1.18.4",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.101",
"@effect/platform-node-shared": "4.0.0-beta.98",
},
"devDependencies": {
"@effect/platform-node": "catalog:",
@@ -584,7 +583,7 @@
"@opencode-ai/client": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "1.18.5",
"@standard-schema/spec": "catalog:",
"@standard-schema/spec": "^1.1.0",
"effect": "catalog:",
"zod": "catalog:",
},
@@ -627,7 +626,6 @@
"name": "@opencode-ai/schema",
"version": "1.17.11",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
},
"devDependencies": {
@@ -672,7 +670,6 @@
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/simulation": "workspace:*",
"@opencode-ai/util": "workspace:*",
"drizzle-orm": "catalog:",
@@ -970,7 +967,6 @@
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@opentelemetry/sdk-trace-base": "2.6.1",
"@opentelemetry/sdk-trace-node": "2.6.1",
"cross-spawn": "catalog:",
"effect": "catalog:",
"glob": "13.0.5",
@@ -990,6 +986,25 @@
"@typescript/native-preview": "catalog:",
},
},
"packages/voice": {
"name": "@opencode-ai/voice",
"version": "0.0.0",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opentui/core": "catalog:",
"@opentui/solid": "catalog:",
"effect": "catalog:",
"opentui-spinner": "catalog:",
"solid-js": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.4",
@@ -1046,16 +1061,16 @@
],
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.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",
"@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",
"@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",
"@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",
"@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",
"@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": {
"@opentui/core": "catalog:",
@@ -1063,14 +1078,13 @@
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"effect": "catalog:",
},
"catalog": {
"@cloudflare/workers-types": "4.20251008.0",
"@corvu/drawer": "0.2.4",
"@effect/opentelemetry": "4.0.0-beta.101",
"@effect/platform-node": "4.0.0-beta.101",
"@effect/sql-sqlite-bun": "4.0.0-beta.101",
"@effect/opentelemetry": "4.0.0-beta.98",
"@effect/platform-node": "4.0.0-beta.98",
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
@@ -1090,7 +1104,6 @@
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@standard-schema/spec": "1.1.0",
"@tailwindcss/vite": "4.1.11",
"@tanstack/solid-virtual": "3.13.32",
"@tsconfig/bun": "1.0.9",
@@ -1107,7 +1120,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.98",
"fuzzysort": "3.1.0",
"get-east-asian-width": "1.6.0",
"hono": "4.10.7",
@@ -1551,13 +1564,13 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@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/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/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": ["@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-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/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/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="],
"@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=="],
"@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=="],
@@ -2083,6 +2096,8 @@
"@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"],
"@opencode-ai/voice": ["@opencode-ai/voice@workspace:packages/voice"],
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@opencode-ai/www": ["@opencode-ai/www@workspace:packages/www"],
@@ -3835,7 +3850,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"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=="],
"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=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
+6 -8
View File
@@ -36,9 +36,9 @@
"packages/slack"
],
"catalog": {
"@effect/opentelemetry": "4.0.0-beta.101",
"@effect/platform-node": "4.0.0-beta.101",
"@effect/sql-sqlite-bun": "4.0.0-beta.101",
"@effect/opentelemetry": "4.0.0-beta.98",
"@effect/platform-node": "4.0.0-beta.98",
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6",
@@ -50,7 +50,6 @@
"@opentui/solid": "0.4.5",
"@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@corvu/drawer": "0.2.4",
@@ -69,7 +68,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.101",
"effect": "4.0.0-beta.98",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"hono": "4.10.7",
@@ -152,8 +151,7 @@
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"effect": "catalog:"
"@types/node": "catalog:"
},
"patchedDependencies": {
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
@@ -167,7 +165,7 @@
"@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",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
}
}
@@ -1,5 +1,4 @@
import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
@@ -20,6 +19,7 @@ import {
type ProviderMetadata,
type ToolCallPart,
type ToolDefinition,
type ToolContent,
type ToolResultPart,
} from "../schema"
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
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
item: Tool.Content,
item: ToolContent,
) {
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 })
@@ -436,7 +436,7 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte
// with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<Tool.Content> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
return yield* Effect.forEach(content, lowerToolResultContentItem)
})
+2 -2
View File
@@ -1,5 +1,4 @@
import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
@@ -17,6 +16,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolContent,
} from "../schema"
import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { GeminiToolSchema } from "./utils/gemini-tool-schema"
@@ -289,7 +289,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
})
continue
}
const content: ReadonlyArray<Tool.Content> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text)
const media: GeminiInlineDataPart[] = []
for (const item of content) {
+4 -4
View File
@@ -1,5 +1,4 @@
import { Effect, Schema } from "effect"
import type { Content } from "@opencode-ai/schema/tool"
import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol"
import {
@@ -15,6 +14,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolContent,
type ToolResultPart,
} from "../schema"
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
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (
item: Content,
item: ToolContent,
request: LLMRequest,
extension: Extension,
) {
@@ -392,7 +392,7 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f
// compatibility with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<Content> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
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))
input.push({ type: "item_reference", id: itemID })
if (store === false && part.result.type === "content") {
const content: ReadonlyArray<Content> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
input.push({
role: "user",
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),
+2 -2
View File
@@ -1,5 +1,4 @@
import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client"
import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint"
@@ -18,6 +17,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolContent,
} from "../schema"
import { classifyProviderFailure } from "../provider-error"
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) })
continue
}
const content: ReadonlyArray<Tool.Content> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
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") })
const files = content.filter((item) => item.type === "file")
+2 -2
View File
@@ -1,5 +1,4 @@
import { Buffer } from "node:buffer"
import { Tool } from "@opencode-ai/schema/tool"
import { Effect, Schema, Stream } from "effect"
import * as Sse from "effect/unstable/encoding/Sse"
import { Headers, HttpClientRequest } from "effect/unstable/http"
@@ -10,6 +9,7 @@ import {
type ContentPart,
type LLMRequest,
type MediaPart,
type ToolFileContent,
type TextPart,
type ToolResultPart,
} 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
})
export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) =>
export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")
+5 -2
View File
@@ -1,5 +1,4 @@
import { Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
export const ProviderFailureClassification = Schema.Literal("context-overflow")
@@ -153,4 +152,8 @@ export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
* Anything thrown or yielded by a handler that is not a `ToolFailure` is
* treated as a defect and fails the stream.
*/
export class ToolFailure extends Tool.Error {}
export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", {
message: Schema.String,
error: Schema.optional(Schema.Defect()),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {}
+7 -5
View File
@@ -1,5 +1,5 @@
import { Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm"
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
import { isRecord } from "../utils/record"
@@ -40,6 +40,8 @@ export const MediaPart = Schema.Struct({
}).annotate({ identifier: "LLM.Content.Media" })
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
export { ToolContent, ToolFileContent, ToolTextContent }
const isToolResultValue = (value: unknown): value is ToolResultValue =>
isRecord(value) &&
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
@@ -61,7 +63,7 @@ export const ToolResultValue = Object.assign(
}),
Schema.Struct({
type: Schema.Literal("content"),
value: Schema.Array(Tool.Content),
value: Schema.Array(ToolContent),
}),
]).annotate({ identifier: "LLM.ToolResult" }),
{
@@ -77,16 +79,16 @@ export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
export interface ToolOutput {
readonly structured: unknown
readonly content: ReadonlyArray<Tool.Content>
readonly content: ReadonlyArray<ToolContent>
}
export const ToolOutput = Object.assign(
Schema.Struct({
structured: Schema.Unknown,
content: Schema.Array(Tool.Content),
content: Schema.Array(ToolContent),
}).annotate({ identifier: "LLM.ToolOutput" }),
{
make: (structured: unknown, content: ReadonlyArray<Tool.Content> = []): ToolOutput => ({ structured, content }),
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({ structured, content }),
fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
switch (result.type) {
case "json":
+1 -1
View File
@@ -28,7 +28,7 @@ export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<Dispat
return decodeAndExecute(tool, call).pipe(
Effect.map((value) => result(call, value)),
Effect.catchTag("Tool.Error", (failure) =>
Effect.catchTag("LLM.ToolFailure", (failure) =>
Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)),
),
)
+6 -6
View File
@@ -1,7 +1,7 @@
import { Effect, JsonSchema, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import type {
ToolCallPart,
ToolContent,
ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType,
} from "./schema"
@@ -31,7 +31,7 @@ export interface ToolModelOutputInput<Parameters, Output> {
export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>,
) => ReadonlyArray<Tool.Content>
) => ReadonlyArray<ToolContent>
/**
* A type-safe LLM tool. Each tool bundles its own description, parameter
@@ -95,7 +95,7 @@ type DynamicToolConfig = {
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
readonly toStructuredOutput?: (output: unknown) => unknown
}
@@ -151,7 +151,7 @@ export function make(config: {
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
readonly toStructuredOutput?: (output: unknown) => unknown
}): AnyExecutableTool
export function make(config: {
@@ -159,7 +159,7 @@ export function make(config: {
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: undefined
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
readonly toStructuredOutput?: (output: unknown) => unknown
}): AnyTool
export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
@@ -236,7 +236,7 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
}
const project = (
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<Tool.Content>) | undefined,
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<ToolContent>) | undefined,
toStructuredOutput: ((output: unknown) => unknown) | undefined,
parameters: unknown,
callID: ToolCallPart["id"],
+2 -2
View File
@@ -1,5 +1,4 @@
import { describe, expect } from "bun:test"
import { Content } from "@opencode-ai/schema/tool"
import { Effect, Schema, Stream } from "effect"
import {
GenerationOptions,
@@ -8,6 +7,7 @@ import {
LLMRequest,
LLMResponse,
ToolChoice,
ToolContent,
ToolOutput,
toDefinitions,
} from "../src"
@@ -279,7 +279,7 @@ describe("LLMClient tools", () => {
it.effect("models canonical tool files with URIs", () =>
Effect.sync(() => {
const decode = Schema.decodeUnknownSync(Content)
const decode = Schema.decodeUnknownSync(ToolContent)
expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({
type: "file",
@@ -88,8 +88,12 @@ function SessionTabSlot(props: {
data-titlebar-tab-slot
data-tab-key={props.id}
data-active={props.active()}
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
classList={{ hidden: !session() && !missingSession() && !persisted()?.title }}
class="relative flex min-w-7 flex-shrink"
classList={{
hidden: !session() && !missingSession() && !persisted()?.title,
"w-72 max-w-72": props.active(),
"w-56 max-w-56": !props.active(),
}}
>
<TabNavItem
ref={(el) => {
@@ -144,7 +148,11 @@ function DraftTabSlot(props: {
data-titlebar-tab-slot
data-tab-key={props.id}
data-active={props.active()}
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
class="relative flex min-w-7 flex-shrink"
classList={{
"w-72 max-w-72": props.active(),
"w-56 max-w-56": !props.active(),
}}
>
<DraftTabItem
ref={(el) => {
@@ -8,8 +8,8 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
} from "@opencode-ai/sdk/v2/client"
import type { SessionDiff } from "@/utils/diffs"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
@@ -171,7 +171,7 @@ export function applyDirectoryEvent(input: {
break
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: SessionDiff[] }
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break
}
@@ -5,8 +5,8 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
} from "@opencode-ai/sdk/v2/client"
import type { SessionDiff } from "@/utils/diffs"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
const msg = (id: string, sessionID: string) =>
@@ -32,7 +32,7 @@ describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SessionDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
@@ -63,7 +63,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SessionDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
@@ -4,14 +4,14 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
} from "@opencode-ai/sdk/v2/client"
import type { SessionDiff } from "@/utils/diffs"
export const SESSION_CACHE_LIMIT = 40
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SessionDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
@@ -13,9 +13,9 @@ import type {
ReferenceInfo,
Session,
SessionStatus,
FileDiffInfo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
import type { SessionDiff } from "@/utils/diffs"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
@@ -50,7 +50,7 @@ export type State = {
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: SessionDiff[]
[sessionID: string]: FileDiffInfo[]
}
permission: {
[sessionID: string]: PermissionRequest[]
+1 -1
View File
@@ -25,7 +25,7 @@ describe("adaptServerEvent", () => {
} as OpenCodeEvent
expect(adaptServerEvent(current)).toMatchObject({
type: "permission.v2.asked",
type: "permission.asked",
properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] },
current,
})
+8 -16
View File
@@ -1,5 +1,5 @@
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import type { Event, PermissionRequest } from "@opencode-ai/sdk/v2/client"
import type { Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { makeEventListener } from "@solid-primitives/event-listener"
@@ -18,15 +18,7 @@ const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true
type PermissionEvent = {
id: string
type: "permission.v2.asked"
properties: PermissionRequest
current?: OpenCodeEvent
}
export type ServerEvent = (Event | PermissionEvent) & {
current?: OpenCodeEvent
}
export type ServerEvent = Event & { current?: OpenCodeEvent }
type QueuedServerEvent = { directory: string; payload: ServerEvent }
type CurrentDelta = Extract<
OpenCodeEvent,
@@ -37,7 +29,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
if (event.type === "permission.v2.asked") {
return {
id: event.id,
type: "permission.v2.asked",
type: "permission.asked",
properties: {
id: event.data.id,
sessionID: event.data.sessionID,
@@ -51,16 +43,16 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
: undefined,
},
current: event,
}
} as ServerEvent
}
if (event.type === "permission.v2.replied")
return { id: event.id, type: "permission.v2.replied", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.asked")
return { id: event.id, type: "question.v2.asked", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.replied")
return { id: event.id, type: "question.v2.replied", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent
if (event.type === "question.v2.rejected")
return { id: event.id, type: "question.v2.rejected", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent
return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent
}
+3 -3
View File
@@ -8,8 +8,8 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
} from "@opencode-ai/sdk/v2/client"
import type { SessionDiff } from "@/utils/diffs"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs"
@@ -140,7 +140,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>,
session_diff: {} as Record<string, SessionDiff[]>,
session_diff: {} as Record<string, FileDiffInfo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>,
message: {} as Record<string, Message[]>,
@@ -773,7 +773,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: SessionDiff[] }
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
return
}
+7 -2
View File
@@ -909,8 +909,13 @@ export default function Page() {
)
const stopVcs = sdk().event.listen((evt) => {
if (evt.details.type !== "file.watcher.updated") return
if (evt.details.properties.file.startsWith(".git/")) return
if (evt.details.type !== "filesystem.changed") return
const props =
typeof evt.details.properties === "object" && evt.details.properties
? (evt.details.properties as Record<string, unknown>)
: undefined
const file = typeof props?.file === "string" ? props.file : undefined
if (!file || file.startsWith(".git/")) return
refreshVcs()
})
onCleanup(stopVcs)
@@ -1,7 +1,6 @@
import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SessionDiff } from "@/utils/diffs"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import type {
SessionReviewCommentActions,
@@ -15,7 +14,7 @@ import type { LineComment } from "@/context/comments"
export type DiffStyle = "unified" | "split"
type ReviewDiff = SessionDiff | VcsFileDiff
type ReviewDiff = FileDiffInfo | VcsFileDiff
export interface SessionReviewTabProps {
title?: JSX.Element
@@ -23,8 +23,7 @@ import { Mark } from "@opencode-ai/ui/logo"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import type { VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SessionDiff } from "@/utils/diffs"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -60,7 +59,7 @@ import type { RenderDiff } from "@/pages/session/v2/review-diff-kinds"
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (SessionDiff | VcsFileDiff)[]
diffs: () => (FileDiffInfo | VcsFileDiff)[]
diffsReady: () => boolean
empty: () => string
hasReview: () => boolean
@@ -1,9 +1,8 @@
import type { VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SessionDiff } from "@/utils/diffs"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Kind } from "@/components/file-tree-v2"
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
export type RenderDiff = SessionDiff | VcsFileDiff
export type RenderDiff = FileDiffInfo | VcsFileDiff
export function normalizePath(p: string) {
return normalizeFileTreeV2Path(p)
@@ -1,6 +1,5 @@
import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js"
import type { VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SessionDiff } from "@/utils/diffs"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
@@ -30,7 +29,7 @@ import {
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
type ReviewDiff = SessionDiff | VcsFileDiff
type ReviewDiff = FileDiffInfo | VcsFileDiff
export type ReviewPanelV2Props = {
title?: JSX.Element
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
@@ -9,7 +9,7 @@ const item = {
additions: 1,
deletions: 1,
status: "modified",
} satisfies SnapshotFileDiff
} satisfies FileDiffInfo
describe("diffs", () => {
test("keeps valid arrays", () => {
+4 -4
View File
@@ -1,9 +1,9 @@
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
export type SessionDiff = SnapshotFileDiff & { file: string; patch: string }
type Diff = FileDiffInfo
function diff(value: unknown): value is SessionDiff {
function diff(value: unknown): value is Diff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
if (!("file" in value) || typeof value.file !== "string") return false
if (!("patch" in value) || typeof value.patch !== "string") return false
@@ -17,7 +17,7 @@ function object(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}
export function diffs(value: unknown): SessionDiff[] {
export function diffs(value: unknown): Diff[] {
if (Array.isArray(value) && value.every(diff)) return value
if (Array.isArray(value)) return value.filter(diff)
if (diff(value)) return [value]
+1 -1
View File
@@ -71,7 +71,7 @@ export async function streamTurn(input: {
const next = await stream.next()
if (next.done) throw new Error("event stream disconnected during prompt execution")
const event = next.value
if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) {
if (event.type === "permission.v2.asked" && event.data.sessionID === input.sessionID) {
const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined
await replyPermission({
client: input.client,
+1 -1
View File
@@ -5,7 +5,7 @@ import { Result } from "effect"
import { isAbsolute, resolve } from "node:path"
import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool"
type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.asked" }>
type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.v2.asked" }>
type Connection = Pick<AgentSideConnection, "requestPermission"> & Partial<Pick<AgentSideConnection, "writeTextFile">>
type Tool = { readonly name: string; readonly input: ToolInput }
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bun
import "@opentui/solid/runtime-plugin-support"
import { NodeRuntime, NodeServices } from "@effect/platform-node"
import { Effect } from "effect"
import { Commands } from "./commands/commands"
@@ -9,8 +9,8 @@ import {
Provider,
Reference,
Skill,
} from "@opencode-ai/plugin/effect"
import { Tool } from "@opencode-ai/schema/tool"
} from "@opencode-ai/plugin/v2/effect"
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
const key = Symbol.for("opencode.plugin.v2.effect")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
@@ -24,5 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.effect")
Provider,
Reference,
Skill,
Tool: { Error: Tool.Error },
Tool,
}
@@ -9,7 +9,8 @@ import {
Provider,
Reference,
Skill,
} from "@opencode-ai/plugin"
} from "@opencode-ai/plugin/v2"
import { Tool } from "@opencode-ai/plugin/v2/tool"
const key = Symbol.for("opencode.plugin.v2.promise")
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
@@ -23,4 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.promise")
Provider,
Reference,
Skill,
Tool,
}
+1 -1
View File
@@ -177,7 +177,7 @@ export async function runNonInteractivePrompt(input: Input) {
}
const event = next.value
if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) {
if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) {
await replyPermission(event.data)
continue
}
+1 -3
View File
@@ -13,13 +13,11 @@ type Options = {
readonly command?: ReadonlyArray<string>
}
const startupDirectory = process.cwd()
function command(password: string, options: Options) {
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
if (!executable) throw new Error("Failed to resolve standalone server command")
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
cwd: startupDirectory,
cwd: process.cwd(),
// Explicit entry wins over anything inherited, so a user-exported
// OPENCODE_PASSWORD cannot shadow the child's lease credential.
env: { OPENCODE_PASSWORD: password },
+3 -5
View File
@@ -1,13 +1,11 @@
import path from "node:path"
const entrypoint = process.argv[1] ? path.resolve(process.argv[1]) : undefined
export function selfCommand() {
const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase()
if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath]
if (!entrypoint) throw new Error("Failed to resolve CLI entrypoint")
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), entrypoint]
return [process.execPath, entrypoint]
if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint")
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]]
return [process.execPath, process.argv[1]]
}
function nodeFlags() {
@@ -502,7 +502,7 @@ function permissionAsked(
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string }
} = {},
) {
return ephemeralEvent("permission.asked", {
return ephemeralEvent("permission.v2.asked", {
id,
sessionID,
action: input.action ?? "shell",
@@ -3,14 +3,12 @@ import { Service } from "@opencode-ai/client/effect/service"
import path from "node:path"
import { Standalone } from "../../src/services/standalone"
process.chdir(path.join(import.meta.dir, "../../../.."))
process.argv[1] = path.join(import.meta.dir, "../../src/index.ts")
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const endpoint = yield* Standalone.start({
command: [process.execPath, path.join(import.meta.dir, "../../src/index.ts"), "serve"],
})
const endpoint = yield* Standalone.start()
const response = yield* Effect.promise(() =>
fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }),
)
+20 -11
View File
@@ -75,11 +75,20 @@ export const define = sdk.Plugin.define`
const effectPluginModule = promisePluginModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin")
const promiseToolModule = `export {}`
const promiseToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
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")]
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
export const Error = sdk.Tool.Error
`
export const Tool = sdk.Tool
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(" ")}
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"
@@ -91,17 +100,17 @@ const __filename = import.meta.filename
const __dirname = import.meta.dirname
const require = __cjs_mod__.createRequire(import.meta.url)
const __ocPluginModules = ${JSON.stringify({
"@opencode-ai/plugin": "opencode:plugin-v2",
"@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin",
"@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool",
"@opencode-ai/plugin/effect": "opencode:plugin-v2-effect",
"@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin",
"@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool",
"@opencode-ai/plugin/v2": "opencode:plugin-v2",
"@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin",
"@opencode-ai/plugin/v2/tool": "opencode:plugin-v2-tool",
"@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect",
"@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin",
"@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool",
})}
const __ocPluginSources = ${JSON.stringify({
"opencode:plugin-v2": promiseModule,
"opencode:plugin-promise-plugin": promisePluginModule,
"opencode:plugin-promise-tool": promiseToolModule,
"opencode:plugin-v2-plugin": promisePluginModule,
"opencode:plugin-v2-tool": promiseToolModule,
"opencode:plugin-v2-effect": effectModule,
"opencode:plugin-v2-effect-plugin": effectPluginModule,
"opencode:plugin-v2-effect-tool": effectToolModule,
+1 -1
View File
@@ -36,7 +36,7 @@
"@opencode-ai/protocol": "workspace:*"
},
"peerDependencies": {
"effect": "4.0.0-beta.101"
"effect": "4.0.0-beta.98"
},
"peerDependenciesMeta": {
"effect": {
+3 -104
View File
@@ -6,86 +6,11 @@ import {
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
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 { Effect } from "effect"
import { fileURLToPath } from "url"
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
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(
Effect.all(
@@ -97,40 +22,14 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/promise/generated", import.meta.url)),
),
write(
emitEffectImported(effectContract, {
module: "../../contract",
api: "ClientApi",
shapeModule: "../api/api.js",
}),
emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }),
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
),
write(
emitEffectShape(effectContract, {
typeReferences: effectTypeReferences,
outputTypes: {
"event.subscribe": {
name: "OpenCodeEvent",
import: 'import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"',
},
},
}),
emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }),
fileURLToPath(new URL("../src/effect/api", import.meta.url)),
),
],
{ concurrency: 3, discard: true },
).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
@@ -7,8 +7,6 @@ import type {
LocationGetOutput,
AgentListInput,
AgentListOutput,
AgentGetInput,
AgentGetOutput,
PluginListInput,
PluginListOutput,
SessionListInput,
@@ -405,18 +403,6 @@ export function make(options: ClientOptions) {
},
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: {
list: (input?: PluginListInput, requestOptions?: RequestOptions) =>
+181 -139
View File
@@ -6,11 +6,11 @@ export type ServiceStopResponse = { accepted: boolean }
export type ModelRef = { id: string; providerID: string; variant?: string }
export type ProviderSettings = { [x: string]: any }
export type ProviderSettings = { [x: string]: JsonValue }
export type AgentColor = string
export type PermissionEffect = "allow" | "deny" | "ask"
export type PermissionV2Effect = "allow" | "deny" | "ask"
export type PluginInfo = { id: string }
@@ -175,24 +175,24 @@ export type ModelCapabilities = { tools: boolean; input: Array<string>; output:
export type ModelVariant = {
id: string
settings?: { [x: string]: any }
settings?: { [x: string]: JsonValue }
headers?: { [x: string]: string }
body?: { [x: string]: any }
body?: { [x: string]: JsonValue }
}
export type MoneyUSDPerMillionTokens = number
export type GenerateTextResponse = { data: { text: string } }
export type ProviderInfo = {
export type ProviderV2Info = {
id: string
integrationID?: string
name: string
disabled?: boolean
package: string
settings?: { [x: string]: any }
settings?: { [x: string]: JsonValue }
headers?: { [x: string]: string }
body?: { [x: string]: any }
body?: { [x: string]: JsonValue }
}
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 PermissionSource = { type: "tool"; messageID: string; callID: string }
export type PermissionV2Source = { type: "tool"; messageID: string; callID: string }
export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string }
@@ -323,9 +323,9 @@ export type FileDiffLegacyInfo = {
status?: "added" | "deleted" | "modified"
}
export type PermissionV1Action = "allow" | "deny" | "ask"
export type PermissionAction = "allow" | "deny" | "ask"
export type SessionV1JSONSchema = { [x: string]: any }
export type JSONSchema = { [x: string]: any }
export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } }
@@ -356,7 +356,7 @@ export type APIError = {
}
}
export type SessionV1TextPart = {
export type TextPart = {
id: string
sessionID: string
messageID: string
@@ -368,7 +368,7 @@ export type SessionV1TextPart = {
metadata?: { [x: string]: any } | undefined
}
export type SessionV1SubtaskPart = {
export type SubtaskPart = {
id: string
sessionID: string
messageID: string
@@ -380,7 +380,7 @@ export type SessionV1SubtaskPart = {
command?: string | undefined
}
export type SessionV1ReasoningPart = {
export type ReasoningPart = {
id: string
sessionID: string
messageID: string
@@ -390,13 +390,13 @@ export type SessionV1ReasoningPart = {
time: { start: number; end?: number | undefined }
}
export type SessionV1FilePartSourceText = { value: string; start: number; end: number }
export type FilePartSourceText = { value: string; start: number; end: number }
export type SessionV1Range = { start: { line: number; character: number }; end: { line: number; character: number } }
export type Range = { start: { line: number; character: number }; end: { line: number; character: number } }
export type SessionV1ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string }
export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string }
export type SessionV1ToolStateRunning = {
export type ToolStateRunning = {
status: "running"
input: { [x: string]: any }
title?: string | undefined
@@ -404,7 +404,7 @@ export type SessionV1ToolStateRunning = {
time: { start: number }
}
export type SessionV1ToolStateError = {
export type ToolStateError = {
status: "error"
input: { [x: string]: any }
error: string
@@ -412,7 +412,7 @@ export type SessionV1ToolStateError = {
time: { start: number; end: number }
}
export type SessionV1StepStartPart = {
export type StepStartPart = {
id: string
sessionID: string
messageID: string
@@ -420,7 +420,7 @@ export type SessionV1StepStartPart = {
snapshot?: string | undefined
}
export type SessionV1StepFinishPart = {
export type StepFinishPart = {
id: string
sessionID: string
messageID: string
@@ -437,15 +437,9 @@ export type SessionV1StepFinishPart = {
}
}
export type SessionV1SnapshotPart = {
id: string
sessionID: string
messageID: string
type: "snapshot"
snapshot: string
}
export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string }
export type SessionV1PatchPart = {
export type PatchPart = {
id: string
sessionID: string
messageID: string
@@ -454,7 +448,7 @@ export type SessionV1PatchPart = {
files: Array<string>
}
export type SessionV1AgentPart = {
export type AgentPart = {
id: string
sessionID: string
messageID: string
@@ -463,7 +457,7 @@ export type SessionV1AgentPart = {
source?: { value: string; start: number; end: number } | undefined
}
export type SessionV1CompactionPart = {
export type CompactionPart = {
id: string
sessionID: string
messageID: string
@@ -473,7 +467,7 @@ export type SessionV1CompactionPart = {
tail_start_id?: string | undefined
}
export type PermissionReply = "once" | "always" | "reject"
export type PermissionV2Reply = "once" | "always" | "reject"
export type Pty = {
id: string
@@ -486,11 +480,11 @@ export type Pty = {
exitCode?: number
}
export type QuestionOption = { label: string; description: string }
export type QuestionV2Option = { label: string; description: string }
export type QuestionTool = { messageID: string; callID: string }
export type QuestionV2Tool = { messageID: string; callID: string }
export type QuestionAnswer = Array<string>
export type QuestionV2Answer = Array<string>
export type FormMetadata1 = { [x: string]: any }
@@ -507,6 +501,12 @@ export type SessionStatus =
}
| { type: "busy" }
export type QuestionOption = { label: string; description: string }
export type QuestionTool = { messageID: string; callID: string }
export type QuestionAnswer = Array<string>
export type ShellInfo1 = {
id: string
status: "running" | "exited" | "timeout" | "killed"
@@ -564,10 +564,10 @@ export type CommandInfo = {
export type ProviderRequest = {
settings: ProviderSettings
headers: { [x: string]: string }
body: { [x: string]: any }
body: { [x: string]: JsonValue }
}
export type PermissionRule = { action: string; resource: string; effect: PermissionEffect }
export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect }
export type SessionAgentSelected = {
id: string
@@ -1044,11 +1044,11 @@ export type ShellDeleted = {
data: { id: string }
}
export type QuestionRejected = {
export type QuestionV2Rejected = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.rejected"
type: "question.v2.rejected"
location?: LocationRef
data: { sessionID: string; requestID: string }
}
@@ -1186,6 +1186,41 @@ export type McpResourcesChanged = {
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 = {
id: string
metadata?: { [x: string]: any } | undefined
@@ -1509,21 +1544,21 @@ export type FormMultiselectField = {
export type FormAnswer = { [x: string]: FormValue }
export type PermissionRequest = {
export type PermissionV2Request = {
id: string
sessionID: string
action: string
resources: Array<string>
save?: Array<string>
metadata?: { [x: string]: JsonValue }
source?: PermissionSource
source?: PermissionV2Source
}
export type PermissionAsked = {
export type PermissionV2Asked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "permission.asked"
type: "permission.v2.asked"
location?: LocationRef
data: {
id: string
@@ -1532,17 +1567,17 @@ export type PermissionAsked = {
resources: Array<string>
save?: Array<string>
metadata?: { [x: string]: any }
source?: PermissionSource
source?: PermissionV2Source
}
}
export type PermissionV1Rule = { permission: string; pattern: string; action: PermissionV1Action }
export type PermissionRule = { permission: string; pattern: string; action: PermissionAction }
export type SessionV1OutputFormat =
export type OutputFormat =
| { type: "text" }
| { type: "json_schema"; schema: SessionV1JSONSchema; retryCount?: number | undefined | undefined }
| { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined }
export type SessionV1AssistantMessage = {
export type AssistantMessage = {
id: string
sessionID: string
role: "assistant"
@@ -1577,7 +1612,7 @@ export type SessionV1AssistantMessage = {
finish?: string | undefined
}
export type SessionV1RetryPart = {
export type RetryPart = {
id: string
sessionID: string
messageID: string
@@ -1608,31 +1643,26 @@ export type SessionError = {
}
}
export type SessionV1FileSource = { text: SessionV1FilePartSourceText; type: "file"; path: string }
export type FileSource = { text: FilePartSourceText; type: "file"; path: string }
export type SessionV1ResourceSource = {
text: SessionV1FilePartSourceText
type: "resource"
clientName: string
uri: string
}
export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string }
export type SessionV1SymbolSource = {
text: SessionV1FilePartSourceText
export type SymbolSource = {
text: FilePartSourceText
type: "symbol"
path: string
range: SessionV1Range
range: Range
name: string
kind: number
}
export type PermissionReplied = {
export type PermissionV2Replied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "permission.replied"
type: "permission.v2.replied"
location?: LocationRef
data: { sessionID: string; requestID: string; reply: PermissionReply }
data: { sessionID: string; requestID: string; reply: PermissionV2Reply }
}
export type PtyCreated = {
@@ -1653,21 +1683,21 @@ export type PtyUpdated = {
data: { info: Pty }
}
export type QuestionInfo = {
export type QuestionV2Info = {
question: string
header: string
options: Array<QuestionOption>
options: Array<QuestionV2Option>
multiple?: boolean
custom?: boolean
}
export type QuestionReplied = {
export type QuestionV2Replied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.replied"
type: "question.v2.replied"
location?: LocationRef
data: { sessionID: string; requestID: string; answers: Array<QuestionAnswer> }
data: { sessionID: string; requestID: string; answers: Array<QuestionV2Answer> }
}
export type FormStringField1 = {
@@ -1744,9 +1774,26 @@ export type SessionStatus2 = {
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 PermissionRuleset = Array<PermissionRule>
export type PermissionV2Ruleset = Array<PermissionV2Rule>
export type SessionInfo = {
id: string
@@ -1863,9 +1910,9 @@ export type ModelInfo = {
name: string
compatibility?: ModelCompatibility
package?: string
settings?: { [x: string]: any }
settings?: { [x: string]: JsonValue }
headers?: { [x: string]: string }
body?: { [x: string]: any }
body?: { [x: string]: JsonValue }
capabilities: ModelCapabilities
variants: Array<ModelVariant>
time: { released: number }
@@ -1901,14 +1948,14 @@ export type FormReplied = {
data: { id: string; sessionID: string; answer: FormAnswer }
}
export type PermissionV1Ruleset = Array<PermissionV1Rule>
export type PermissionRuleset = Array<PermissionRule>
export type SessionV1UserMessage = {
export type UserMessage = {
id: string
sessionID: string
role: "user"
time: { created: number }
format?: SessionV1OutputFormat | undefined
format?: OutputFormat | undefined
summary?: { title?: string | undefined; body?: string | undefined; diffs: Array<FileDiffLegacyInfo> } | undefined
agent: string
model: { providerID: string; modelID: string; variant?: string | undefined }
@@ -1916,18 +1963,23 @@ export type SessionV1UserMessage = {
tools?: { [x: string]: boolean } | undefined
}
export type SessionV1FilePartSource = SessionV1FileSource | SessionV1SymbolSource | SessionV1ResourceSource
export type FilePartSource = FileSource | SymbolSource | ResourceSource
export type QuestionAsked = {
export type QuestionV2Asked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "question.asked"
type: "question.v2.asked"
location?: LocationRef
data: { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
data: { id: string; sessionID: string; questions: Array<QuestionV2Info>; tool?: QuestionV2Tool }
}
export type QuestionRequest = { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
export type QuestionV2Request = {
id: string
sessionID: string
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
}
export type FormField1 =
| FormStringField1
@@ -1937,6 +1989,15 @@ export type FormField1 =
| FormMultiselectField1
| 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 = {
name: string
path: string
@@ -1956,7 +2017,7 @@ export type AgentInfo = {
hidden: boolean
color?: AgentColor
steps?: number
permissions: PermissionRuleset
permissions: PermissionV2Ruleset
}
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
@@ -2014,13 +2075,13 @@ export type SessionV1Info = {
version: string
metadata?: { [x: string]: any }
time: { created: number; updated: number; compacting?: number; archived?: number }
permission?: PermissionV1Ruleset
permission?: PermissionRuleset
revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string }
}
export type SessionV1Message = SessionV1UserMessage | SessionV1AssistantMessage
export type Message = UserMessage | AssistantMessage
export type SessionV1FilePart = {
export type FilePart = {
id: string
sessionID: string
messageID: string
@@ -2028,7 +2089,7 @@ export type SessionV1FilePart = {
mime: string
filename?: string | undefined
url: string
source?: SessionV1FilePartSource | undefined
source?: FilePartSource | undefined
}
export type FormFields1 = [FormField1, ...Array<FormField1>]
@@ -2099,17 +2160,17 @@ export type MessageUpdated = {
type: "message.updated"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; info: SessionV1Message }
data: { sessionID: string; info: Message }
}
export type SessionV1ToolStateCompleted = {
export type ToolStateCompleted = {
status: "completed"
input: { [x: string]: any }
output: string
title: string
metadata: { [x: string]: any }
time: { start: number; end: number; compacted?: number | undefined }
attachments?: Array<SessionV1FilePart> | undefined
attachments?: Array<FilePart> | undefined
}
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 }
@@ -2135,11 +2196,7 @@ export type SessionMessageInfo =
| SessionMessageAssistant
| SessionMessageCompaction
export type SessionV1ToolState =
| SessionV1ToolStatePending
| SessionV1ToolStateRunning
| SessionV1ToolStateCompleted
| SessionV1ToolStateError
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export type FormCreated = {
id: string
@@ -2195,32 +2252,32 @@ export type SessionMessagesResponse = {
cursor: { previous?: string | null; next?: string | null }
}
export type SessionV1ToolPart = {
export type ToolPart = {
id: string
sessionID: string
messageID: string
type: "tool"
callID: string
tool: string
state: SessionV1ToolState
state: ToolState
metadata?: { [x: string]: any } | undefined
}
export type SessionLogItem = SessionEventDurable | EventLogSynced
export type SessionV1Part =
| SessionV1TextPart
| SessionV1SubtaskPart
| SessionV1ReasoningPart
| SessionV1FilePart
| SessionV1ToolPart
| SessionV1StepStartPart
| SessionV1StepFinishPart
| SessionV1SnapshotPart
| SessionV1PatchPart
| SessionV1AgentPart
| SessionV1RetryPart
| SessionV1CompactionPart
export type Part =
| TextPart
| SubtaskPart
| ReasoningPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| PatchPart
| AgentPart
| RetryPart
| CompactionPart
export type MessagePartUpdated = {
id: string
@@ -2229,7 +2286,7 @@ export type MessagePartUpdated = {
type: "message.part.updated"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; part: SessionV1Part; time: number }
data: { sessionID: string; part: Part; time: number }
}
export type V2Event =
@@ -2290,8 +2347,8 @@ export type V2Event =
| SessionRevertCommitted
| FilesystemChanged
| ReferenceUpdated
| PermissionAsked
| PermissionReplied
| PermissionV2Asked
| PermissionV2Replied
| PluginAdded
| PluginUpdated
| ProjectDirectoriesUpdated
@@ -2305,9 +2362,9 @@ export type V2Event =
| ShellCreated
| ShellExited
| ShellDeleted
| QuestionAsked
| QuestionReplied
| QuestionRejected
| QuestionV2Asked
| QuestionV2Replied
| QuestionV2Rejected
| FormCreated
| FormReplied
| FormCancelled
@@ -2323,6 +2380,11 @@ export type V2Event =
| VcsBranchUpdated
| McpStatusChanged
| McpResourcesChanged
| PermissionAsked
| PermissionReplied
| QuestionAsked
| QuestionReplied
| QuestionRejected
| SessionError
| V2EventServerConnected
@@ -2339,14 +2401,6 @@ export type InvalidRequestError = {
export const isInvalidRequestError = (value: unknown): value is 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 const isInvalidCursorError = (value: unknown): value is InvalidCursorError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError"
@@ -2530,18 +2584,6 @@ export type AgentListOutput = {
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 = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -3270,7 +3312,7 @@ export type ProviderListInput = {
export type ProviderListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ProviderInfo>
data: Array<ProviderV2Info>
}
export type ProviderGetInput = {
@@ -3282,7 +3324,7 @@ export type ProviderGetInput = {
export type ProviderGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ProviderInfo
data: ProviderV2Info
}
export type IntegrationListInput = {
@@ -4434,7 +4476,7 @@ export type PermissionRequestListInput = {
export type PermissionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<PermissionRequest>
data: Array<PermissionV2Request>
}
export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] }
@@ -4512,18 +4554,18 @@ export type PermissionCreateInput = {
}["agent"]
}
export type PermissionCreateOutput = { data: { id: string; effect: PermissionEffect } }["data"]
export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"]
export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type PermissionListOutput = { data: Array<PermissionRequest> }["data"]
export type PermissionListOutput = { data: Array<PermissionV2Request> }["data"]
export type PermissionGetInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"]
}
export type PermissionGetOutput = { data: PermissionRequest }["data"]
export type PermissionGetOutput = { data: PermissionV2Request }["data"]
export type PermissionReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
@@ -4822,12 +4864,12 @@ export type QuestionRequestListInput = {
export type QuestionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<QuestionRequest>
data: Array<QuestionV2Request>
}
export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type QuestionListOutput = { data: Array<QuestionRequest> }["data"]
export type QuestionListOutput = { data: Array<QuestionV2Request> }["data"]
export type QuestionReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
-3
View File
@@ -37,7 +37,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
let announced = false
let lastSpawn = 0
let spawnDelay = 5_000
let ownerHeld = false
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => {
if (announced) return
@@ -65,7 +64,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
const registration = await registered(options.file, true)
if (registration.service !== undefined) {
ownerHeld = false
spawnDelay = 5_000
const service = registration.service
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
@@ -82,7 +80,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (failure !== undefined) throw failure
const finished = [...contenders].filter(contenderFinished)
if (finished.some((item) => item.child.exitCode === 0)) {
ownerHeld = true
spawnDelay = Math.min(spawnDelay * 2, 30_000)
}
finished.forEach((item) => contenders.delete(item))
+1 -6
View File
@@ -1,6 +1,5 @@
import { Effect } from "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 PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
@@ -10,10 +9,6 @@ declare const promiseClient: PromiseClient
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"]
const effectList: Effect.Effect<
@@ -42,4 +37,4 @@ const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.re
key: "review-notes",
})
void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
@@ -14,14 +14,6 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
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", () => {
const made = Prompt.make({ text: "hello" })
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
-1
View File
@@ -99,7 +99,6 @@
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@standard-schema/spec": "catalog:",
"@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4",
"@openrouter/ai-sdk-provider": "2.9.0",
+1 -1
View File
@@ -1,4 +1,4 @@
export * as Account from "./account"
export * as AccountV2 from "./account"
import { Schema } from "effect"
import type { HttpClientError } from "effect/unstable/http"
+8 -8
View File
@@ -1,14 +1,14 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
import { Account } from "../account"
import { AccountV2 } from "../account"
import { Timestamps } from "../database/schema.sql"
export const AccountTable = sqliteTable("account", {
id: text().$type<Account.ID>().primaryKey(),
id: text().$type<AccountV2.ID>().primaryKey(),
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
token_expiry: integer(),
...Timestamps,
})
@@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", {
export const AccountStateTable = sqliteTable("account_state", {
id: integer().primaryKey(),
active_account_id: text()
.$type<Account.ID>()
.$type<AccountV2.ID>()
.references(() => AccountTable.id, { onDelete: "set null" }),
active_org_id: text().$type<Account.OrgID>(),
active_org_id: text().$type<AccountV2.OrgID>(),
})
// LEGACY
@@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable(
{
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(),
access_token: text().$type<AccountV2.AccessToken>().notNull(),
refresh_token: text().$type<AccountV2.RefreshToken>().notNull(),
token_expiry: integer(),
active: integer({ mode: "boolean" })
.notNull()
+12 -12
View File
@@ -1,9 +1,9 @@
export * as Agent from "./agent"
export * as AgentV2 from "./agent"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { Bus } from "./bus"
import { EventV2 } from "./event"
import { State } from "./state"
export const ID = Agent.ID
@@ -17,7 +17,7 @@ export const Color = Agent.Color
export const Info = Agent.Info
export type Info = Agent.Info
export { Event } from "@opencode-ai/schema/agent"
export const Event = Agent.Event
export interface Selection {
readonly id: ID
@@ -45,12 +45,12 @@ export interface Interface extends State.Transformable<Draft> {
readonly list: () => Effect.Effect<Info[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const state = State.create<Data, Draft>({
name: "agent",
initial: () => ({ agents: new Map() }),
@@ -70,7 +70,7 @@ const layer = Layer.effect(
draft.agents.delete(id)
},
}),
finalize: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
})
const selectable = (agent: Info | undefined) =>
agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
@@ -89,17 +89,17 @@ const layer = Layer.effect(
return Service.of({
transform: state.transform,
reload: state.reload,
get: Effect.fn("Agent.get")(function* (id) {
get: Effect.fn("AgentV2.get")(function* (id) {
return state.get().agents.get(id)
}),
default: Effect.fn("Agent.default")(function* () {
default: Effect.fn("AgentV2.default")(function* () {
return selectedDefault()
}),
resolve: Effect.fn("Agent.resolve")(function* (id) {
resolve: Effect.fn("AgentV2.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault()
}),
select: Effect.fn("Agent.select")(function* (id) {
select: Effect.fn("AgentV2.select")(function* (id) {
if (id !== undefined) {
const selected = ID.make(id)
return { id: selected, info: state.get().agents.get(selected) }
@@ -107,11 +107,11 @@ const layer = Layer.effect(
const info = selectedDefault()
return { id: info?.id ?? defaultID, info }
}),
list: Effect.fn("Agent.list")(function* () {
list: Effect.fn("AgentV2.list")(function* () {
return Array.fromIterable(state.get().agents.values())
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
+20 -20
View File
@@ -32,8 +32,8 @@ import {
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import type { ID, Info } from "./model"
import { Provider } from "./provider"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { State } from "./state"
type SDK = any
@@ -42,14 +42,14 @@ type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
export interface SDKEvent {
readonly model: Info
readonly model: ModelV2.Info
readonly package: string
readonly options: Record<string, any>
sdk?: SDK
}
export interface LanguageEvent {
readonly model: Info
readonly model: ModelV2.Info
readonly sdk: SDK
readonly options: Record<string, any>
language?: LanguageModelV3
@@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
})
}
function prepareOptions(model: Info, pkg: string) {
function prepareOptions(model: ModelV2.Info, pkg: string) {
const projected = mapBodyToProviderOptions(model, pkg)
const options: Record<string, any> = {
name: model.providerID,
@@ -146,7 +146,7 @@ function prepareOptions(model: Info, pkg: string) {
if (typeof opts.body === "string" && model.body !== undefined) {
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body))
opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
}
}
@@ -162,11 +162,11 @@ function prepareOptions(model: Info, pkg: string) {
}
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: Provider.ID,
providerID: ProviderV2.ID,
cause: Schema.Defect(),
}) {}
function initError(providerID: Provider.ID) {
function initError(providerID: ProviderV2.ID) {
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 runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: Info) => Effect.Effect<Model, InitError>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
export const locationLayer = Layer.effect(
Service,
@@ -260,13 +260,13 @@ export const locationLayer = Layer.effect(
})
const existing = languages.get(key)
if (existing) return existing
if (!Provider.isAISDK(model.package))
if (!ProviderV2.isAISDK(model.package))
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported package ${model.package}`),
})
const packageName = Provider.packageName(model.package)
const packageName = ProviderV2.packageName(model.package)
const options = prepareOptions(model, packageName)
const sdkKey = cacheKey({
providerID: model.providerID,
@@ -301,8 +301,8 @@ export const locationLayer = Layer.effect(
export const defaultLayer = locationLayer
function modelFromLanguage(info: Info, language: LanguageModelV3) {
const packageName = Provider.packageName(info.package!)
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const packageName = ProviderV2.packageName(info.package!)
const projected = mapBodyToProviderOptions(info, packageName)
const optionKey = providerOptionKey(packageName, info.providerID)
const providerOptions = (() => {
@@ -352,7 +352,7 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
})
}
function gatewayProviderOptions(modelID: ID, settings: Readonly<Record<string, unknown>>) {
function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
const gateway =
typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
? Object.fromEntries(Object.entries(settings.gateway))
@@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ID, settings: Readonly<Record<string, u
return { gateway: model }
}
function providerOptionKey(packageName: string | undefined, providerID: Provider.ID) {
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
if (packageName === "@ai-sdk/google") return "google"
if (packageName === "@ai-sdk/google-vertex") return "vertex"
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
}
function mapBodyToProviderOptions(model: Info, packageName: string) {
function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
const settings = requestSettings(model.settings)
const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning)
const forceReasoning =
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) &&
(pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined)
const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings
const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
if (!pro) return { settings: normalized, body: model.body }
const body = { ...model.body }
delete body.reasoning
return {
settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }),
settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
body: Object.keys(body).length === 0 ? undefined : body,
}
}
+51 -51
View File
@@ -3,82 +3,82 @@ export * as Catalog from "./catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Order, pipe } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { Model } from "./model"
import { Provider } from "./provider"
import { Bus } from "./bus"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { State } from "./state"
import { Integration } from "./integration"
export type ProviderRecord = {
provider: Provider.MutableInfo
models: Map<Model.ID, Model.MutableInfo>
provider: ProviderV2.MutableInfo
models: Map<ModelV2.ID, ModelV2.MutableInfo>
}
export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID }
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export { Event } from "@opencode-ai/schema/catalog"
export const Event = Catalog.Event
type Data = {
providers: Map<Provider.ID, ProviderRecord>
providers: Map<ProviderV2.ID, ProviderRecord>
defaultModel?: DefaultModel
}
export type Draft = {
provider: {
list: () => readonly ProviderRecord[]
get: (providerID: Provider.ID) => ProviderRecord | undefined
update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void
remove: (providerID: Provider.ID) => void
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID) => void
}
model: {
get: (providerID: Provider.ID, modelID: Model.ID) => Model.Info | undefined
update: (providerID: Provider.ID, modelID: Model.ID, fn: (model: Model.MutableInfo) => void) => void
remove: (providerID: Provider.ID, modelID: Model.ID) => void
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
default: {
get: () => DefaultModel | undefined
set: (providerID: Provider.ID, modelID: Model.ID) => void
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
}
}
}
export interface Interface extends State.Transformable<Draft> {
readonly provider: {
readonly get: (providerID: Provider.ID) => Effect.Effect<Provider.Info | undefined>
readonly all: () => Effect.Effect<Provider.Info[]>
readonly available: () => Effect.Effect<Provider.Info[]>
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
readonly all: () => Effect.Effect<ProviderV2.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]>
}
readonly model: {
readonly get: (providerID: Provider.ID, modelID: Model.ID) => Effect.Effect<Model.Info | undefined>
readonly all: () => Effect.Effect<Model.Info[]>
readonly available: () => Effect.Effect<Model.Info[]>
readonly default: () => Effect.Effect<Model.Info | undefined>
readonly small: (providerID: Provider.ID) => Effect.Effect<Model.Info | undefined>
readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined>
readonly all: () => Effect.Effect<ModelV2.Info[]>
readonly available: () => Effect.Effect<ModelV2.Info[]>
readonly default: () => Effect.Effect<ModelV2.Info | undefined>
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined>
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Catalog") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const integrations = yield* Integration.Service
const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration
}
const projectModel = (model: Model.Info, provider: Provider.Info) => {
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
return {
...model,
package: model.package ?? provider.package,
settings: Provider.mergeOverlay(provider.settings, model.settings),
headers: Provider.mergeHeaders(provider.headers, model.headers),
body: Provider.mergeOverlay(provider.body, model.body),
} satisfies Model.Info
settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body),
} satisfies ModelV2.Info
}
const state = State.create<Data, Draft>({
@@ -93,8 +93,8 @@ const layer = Layer.effect(
let current = draft.providers.get(providerID)
if (!current) {
current = {
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
}
draft.providers.set(providerID, current)
}
@@ -110,13 +110,13 @@ const layer = Layer.effect(
let record = draft.providers.get(providerID)
if (!record) {
record = {
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
}
draft.providers.set(providerID, record)
}
const model =
record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model)
model.id = modelID
@@ -135,8 +135,8 @@ const layer = Layer.effect(
}
return result
},
finalize: Effect.fn("Catalog.finalize")(function* (catalog) {
yield* bus.publish(Catalog.Event.Updated, {})
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
yield* events.publish(Event.Updated, {})
}),
})
const result: Interface = {
@@ -144,15 +144,15 @@ const layer = Layer.effect(
reload: state.reload,
provider: {
get: Effect.fn("Catalog.provider.get")(function* (providerID) {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
return state.get().providers.get(providerID)?.provider
}),
all: Effect.fn("Catalog.provider.all")(function* () {
all: Effect.fn("CatalogV2.provider.all")(function* () {
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
}),
available: Effect.fn("Catalog.provider.available")(function* () {
available: Effect.fn("CatalogV2.provider.available")(function* () {
const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
return (yield* result.provider.all()).filter((provider) =>
available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))),
@@ -161,14 +161,14 @@ const layer = Layer.effect(
},
model: {
get: Effect.fn("Catalog.model.get")(function* (providerID, modelID) {
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
const record = state.get().providers.get(providerID)
if (!record) return
const model = record.models.get(modelID)
return model && projectModel(model, record.provider)
}),
all: Effect.fn("Catalog.model.all")(function* () {
all: Effect.fn("CatalogV2.model.all")(function* () {
return pipe(
Array.fromIterable(state.get().providers.values()),
Array.flatMap((record) => {
@@ -178,9 +178,9 @@ const layer = Layer.effect(
)
}),
available: Effect.fn("Catalog.model.available")(function* () {
available: Effect.fn("CatalogV2.model.available")(function* () {
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
const models: Model.Info[] = []
const models: ModelV2.Info[] = []
for (const record of state.get().providers.values()) {
if (!providers.has(record.provider.id)) continue
for (const model of record.models.values()) {
@@ -194,7 +194,7 @@ const layer = Layer.effect(
)
}),
default: Effect.fn("Catalog.model.default")(function* () {
default: Effect.fn("CatalogV2.model.default")(function* () {
const defaultModel = state.get().defaultModel
if (defaultModel) {
const provider = yield* result.provider.get(defaultModel.providerID)
@@ -207,18 +207,18 @@ const layer = Layer.effect(
return (yield* result.model.available())[0]
}),
small: Effect.fn("Catalog.model.small")(function* (providerID) {
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
const record = state.get().providers.get(providerID)
if (!record) return
const provider = record.provider
// TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments.
if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) {
if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) {
return
}
if (providerID === Provider.ID.opencode) {
const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano"))
if (providerID === ProviderV2.ID.opencode) {
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
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/
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
+77
View File
@@ -0,0 +1,77 @@
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: [] })
+10 -10
View File
@@ -1,11 +1,11 @@
export * as Command from "./command"
export * as CommandV2 from "./command"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { Command } from "@opencode-ai/schema/command"
import { State } from "./state"
import { MCP } from "./mcp/index"
import { Bus } from "./bus"
import { EventV2 } from "./event"
import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process"
import { Config } from "./config"
@@ -14,7 +14,7 @@ import { ShellSelect } from "./shell/select"
export const Info = Command.Info
export type Info = Command.Info
export { Event } from "@opencode-ai/schema/command"
export const Event = Command.Event
export type Evaluation = {
readonly text: string
@@ -50,13 +50,13 @@ export interface Interface extends State.Transformable<Draft> {
}) => Effect.Effect<Evaluation, NotFoundError | EvaluationError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {}
export const layer = (options?: ShellSelect.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const mcp = yield* MCP.Service
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const processes = yield* AppProcess.Service
const config = yield* Config.Service
const location = yield* Location.Service
@@ -76,7 +76,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
draft.commands.delete(name)
},
}),
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
})
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () {
@@ -92,12 +92,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
return Service.of({
reload: state.reload,
transform: state.transform,
get: Effect.fn("Command.get")(function* (name) {
get: Effect.fn("CommandV2.get")(function* (name) {
const command = staticCommand(name)
if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name)
}),
list: Effect.fn("Command.list")(function* () {
list: Effect.fn("CommandV2.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[]
const names = new Set(commands.map((command) => command.name))
return [
@@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
]
}),
evaluate: Effect.fn("Command.evaluate")(function* (input) {
evaluate: Effect.fn("CommandV2.evaluate")(function* (input) {
const command = staticCommand(input.name)
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
config,
@@ -247,7 +247,7 @@ export function configured(options?: ShellSelect.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node],
deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node],
})
}
+7 -7
View File
@@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { Bus } from "./bus"
import { EventV2 } from "./event"
import { Watcher } from "./filesystem/watcher"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Global } from "@opencode-ai/util/global"
@@ -168,7 +168,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {}
export const layer = (options?: Options) => Layer.effect(
Service,
@@ -177,7 +177,7 @@ export const layer = (options?: Options) => Layer.effect(
const global = yield* Global.Service
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const credentials = yield* Credential.Service
const wellknown = yield* WellKnown.Service
const names = ["opencode.json", "opencode.jsonc"]
@@ -375,7 +375,7 @@ export const layer = (options?: Options) => Layer.effect(
if (isDeepStrictEqual(configs, next)) return
configs = next
yield* reconcile(next)
yield* bus.publish(ConfigSchema.Event.Updated, {})
yield* events.publish(ConfigSchema.Event.Updated, {})
}),
),
)
@@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect(
),
Effect.forkScoped({ startImmediately: true }),
)
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) =>
wellknown.entries().pipe(
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 }),
)
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
yield* events.subscribe(WellKnown.Event.Updated).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
),
@@ -438,7 +438,7 @@ export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
})
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { PositiveInt } from "../schema"
export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
export class Info extends Schema.Class<Info>("Config.Agent")({
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
model: ConfigModel.Selection.pipe(Schema.optional),
request: ConfigProvider.Request.pipe(Schema.optional),
system: Schema.String.pipe(Schema.optional),
+2 -2
View File
@@ -3,13 +3,13 @@ export * as ConfigAttachments from "./attachments"
import { Schema } from "effect"
import { PositiveInt } from "../schema"
export class Image extends Schema.Class<Image>("Config.Attachments.Image")({
export class Image extends Schema.Class<Image>("ConfigV2.Attachments.Image")({
auto_resize: Schema.Boolean.pipe(Schema.optional),
max_width: PositiveInt.pipe(Schema.optional),
max_height: PositiveInt.pipe(Schema.optional),
max_base64_bytes: PositiveInt.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("Config.Attachments")({
export class Info extends Schema.Class<Info>("ConfigV2.Attachments")({
image: Image.pipe(Schema.optional),
}) {}
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigCommand from "./command"
import { Schema } from "effect"
import { ConfigModel } from "./model"
export class Info extends Schema.Class<Info>("Config.Command")({
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
template: Schema.String,
description: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional),
+2 -2
View File
@@ -3,11 +3,11 @@ export * as ConfigCompaction from "./compaction"
import { Schema } from "effect"
import { NonNegativeInt } from "../schema"
export class Keep extends Schema.Class<Keep>("Config.Compaction.Keep")({
export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({
tokens: NonNegativeInt.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("Config.Compaction")({
export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({
auto: Schema.Boolean.pipe(Schema.optional),
keep: Keep.pipe(Schema.optional),
buffer: NonNegativeInt.pipe(Schema.optional),
+1 -1
View File
@@ -2,7 +2,7 @@ export * as ConfigFormatter from "./formatter"
import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("Config.Formatter.Entry")({
export class Entry extends Schema.Class<Entry>("ConfigV2.Formatter.Entry")({
disabled: Schema.Boolean.pipe(Schema.optional),
command: Schema.String.pipe(Schema.Array, Schema.optional),
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
+1 -1
View File
@@ -6,7 +6,7 @@ export const Disabled = Schema.Struct({
disabled: Schema.Literal(true),
})
export class Server extends Schema.Class<Server>("Config.LSP.Server")({
export class Server extends Schema.Class<Server>("ConfigV2.LSP.Server")({
command: Schema.String.pipe(Schema.Array),
extensions: Schema.String.pipe(Schema.Array, Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
+1 -1
View File
@@ -15,7 +15,7 @@ export const Remote = Mcp.RemoteConfig
export type Remote = Mcp.RemoteConfig
export const Server = Mcp.ServerConfig
export class Info extends Schema.Class<Info>("Config.MCP")({
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
timeout: Timeout.pipe(Schema.optional),
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
}) {}
+1 -1
View File
@@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin"
import { Schema } from "effect"
export class Entry extends Schema.Class<Entry>("Config.Plugin.Entry")({
export class Entry extends Schema.Class<Entry>("ConfigV2.Plugin.Entry")({
package: Schema.String,
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}) {}
+8 -8
View File
@@ -1,9 +1,9 @@
export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { Agent } from "../../agent"
import { AgentV2 } from "../../agent"
import { Config } from "../../config"
import { ConfigAgent } from "../agent"
import { ConfigMarkdown } from "../markdown"
@@ -11,10 +11,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigAgentV1 } from "../../v1/config/agent"
import { ConfigMigrateV1 } from "../../v1/config/migrate"
import { Global } from "@opencode-ai/util/global"
import { Permission } from "../../permission"
import { PermissionV2 } from "../../permission"
import type { LocationMutation } from "../../location-mutation"
import type { ReadTool } from "../../tool/plugin/read"
import type { EditTool } from "../../tool/plugin/edit"
import type { ReadTool } from "../../tool/read"
import type { EditTool } from "../../tool/edit"
const legacySources = [
{ pattern: "{agent,agents}/**/*.md", primary: false },
@@ -74,14 +74,14 @@ export const Plugin = define({
global.home,
)
const configuredDefault = Config.latest(loaded.documents, "default_agent")
if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault))
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...permissions))
}
for (const document of loaded.documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = Agent.ID.make(id)
const agentID = AgentV2.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
@@ -126,7 +126,7 @@ export const Plugin = define({
}),
})
function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset {
function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset {
// 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.
return rules.map((rule) =>
+2 -2
View File
@@ -1,9 +1,9 @@
export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { Command } from "../../command"
import { CommandV2 } from "../../command"
import { Config } from "../../config"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { ConfigCommand } from "../command"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigPolicyPlugin from "./policy"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { Wildcard } from "../../util/wildcard"
+11 -11
View File
@@ -1,10 +1,10 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { Provider } from "../../provider"
import { ProviderV2 } from "../../provider"
export const Plugin = define({
id: "opencode.config.provider",
@@ -49,9 +49,9 @@ export const Plugin = define({
if (item.name !== undefined) provider.name = item.name
if (item.package !== undefined) provider.package = item.package
if (item.settings !== undefined)
provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
@@ -62,9 +62,9 @@ export const Plugin = define({
model.compatibility = { ...model.compatibility, ...config.compatibility }
if (config.package !== undefined) model.package = config.package
if (config.settings !== undefined)
model.settings = Provider.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
@@ -81,10 +81,10 @@ export const Plugin = define({
model.variants.push(existing)
}
if (variant.settings !== undefined)
existing.settings = Provider.mergeOverlay(existing.settings, variant.settings)
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
if (variant.headers !== undefined)
existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import path from "path"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
+7 -7
View File
@@ -1,11 +1,11 @@
export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import path from "path"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { AbsolutePath } from "../../schema"
import { Skill } from "../../skill"
import { SkillV2 } from "../../skill"
import { Global } from "@opencode-ai/util/global"
import { Location } from "../../location"
@@ -23,7 +23,7 @@ export const Plugin = define({
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of [...claude, ...agents]) {
draft.source(
Skill.DirectorySource.make({
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
@@ -31,10 +31,10 @@ export const Plugin = define({
}
for (const directory of directories) {
draft.source(
Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
draft.source(
Skill.DirectorySource.make({
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
@@ -42,12 +42,12 @@ export const Plugin = define({
}
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
draft.source(Skill.UrlSource.make({ type: "url", url: item }))
draft.source(SkillV2.UrlSource.make({ type: "url", url: item }))
continue
}
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
Skill.DirectorySource.make({
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigWebSearchPlugin from "./websearch"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
+12 -12
View File
@@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model"
import { ModelV2 } from "../model"
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@@ -12,17 +12,17 @@ export const Overlays = {
body: JsonRecord.pipe(Schema.optional),
}
export class Request extends Schema.Class<Request>("Config.Provider.Request")({
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
headers: Overlays.headers,
body: Overlays.body,
}) {}
class Cache extends Schema.Class<Cache>("Config.Model.Cost.Cache")({
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional),
}) {}
class Cost extends Schema.Class<Cost>("Config.Model.Cost")({
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Int,
@@ -32,22 +32,22 @@ class Cost extends Schema.Class<Cost>("Config.Model.Cost")({
cache: Cache.pipe(Schema.optional),
}) {}
class Limit extends Schema.Class<Limit>("Config.Model.Limit")({
class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
context: Schema.Int.pipe(Schema.optional),
input: Schema.Int.pipe(Schema.optional),
output: Schema.Int.pipe(Schema.optional),
}) {}
class Model extends Schema.Class<Model>("Config.Model")({
modelID: ID.pipe(Schema.optional),
family: Family.pipe(Schema.optional),
class Model extends Schema.Class<Model>("ConfigV2.Model")({
modelID: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional),
compatibility: Compatibility.pipe(Schema.optional),
compatibility: ModelV2.Compatibility.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
...Overlays,
capabilities: Capabilities.pipe(Schema.optional),
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
variants: Schema.Struct({
id: VariantID,
id: ModelV2.VariantID,
...Overlays,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
@@ -55,7 +55,7 @@ class Model extends Schema.Class<Model>("Config.Model")({
limit: Limit.pipe(Schema.optional),
}) {}
export class Info extends Schema.Class<Info>("Config.Provider")({
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional),
package: Schema.String.pipe(Schema.optional),
+2 -2
View File
@@ -2,14 +2,14 @@ export * as ConfigReference from "./reference"
import { Schema } from "effect"
export class Git extends Schema.Class<Git>("Config.Reference.Git")({
export class Git extends Schema.Class<Git>("ConfigV2.Reference.Git")({
repository: Schema.String,
branch: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional),
}) {}
export class Local extends Schema.Class<Local>("Config.Reference.Local")({
export class Local extends Schema.Class<Local>("ConfigV2.Reference.Local")({
path: Schema.String,
description: Schema.String.pipe(Schema.optional),
hidden: Schema.Boolean.pipe(Schema.optional),
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigToolOutput from "./tool-output"
import { Schema } from "effect"
import { PositiveInt } from "../schema"
export class Info extends Schema.Class<Info>("Config.ToolOutput")({
export class Info extends Schema.Class<Info>("ConfigV2.ToolOutput")({
max_lines: PositiveInt.pipe(Schema.optional),
max_bytes: PositiveInt.pipe(Schema.optional),
}) {}
+1 -1
View File
@@ -2,7 +2,7 @@ export * as ConfigWarming from "./warming"
import { Schema } from "effect"
export class Info extends Schema.Class<Info>("Config.Warming")({
export class Info extends Schema.Class<Info>("ConfigV2.Warming")({
prompt: Schema.String.pipe(Schema.optional).annotate({
description: "Prompt sent for keep-alive requests",
}),
+1 -1
View File
@@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher"
import { Schema } from "effect"
export class Info extends Schema.Class<Info>("Config.Watcher")({
export class Info extends Schema.Class<Info>("ConfigV2.Watcher")({
ignore: Schema.String.pipe(Schema.Array, Schema.optional),
}) {}
+12 -12
View File
@@ -5,8 +5,8 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git"
import { Global } from "@opencode-ai/util/global"
import { Project } from "../project"
import { Session } from "../session"
import { ProjectV2 } from "../project"
import { SessionV2 } from "../session"
import { SessionExecution } from "../session/execution"
import { SessionSchema } from "../session/schema"
import { SessionStore } from "../session/store"
@@ -28,8 +28,8 @@ export type Input = typeof Input.Type
export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
"MoveSession.DestinationProjectMismatchError",
{
expected: Project.ID,
actual: Project.ID,
expected: ProjectV2.ID,
actual: ProjectV2.ID,
},
) {}
@@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSource
) {}
export type Error =
| Session.NotFoundError
| SessionV2.NotFoundError
| DestinationProjectMismatchError
| DestinationNotFoundError
| DestinationNotDirectoryError
| Session.DestinationNotFoundError
| Session.DestinationNotDirectoryError
| SessionV2.DestinationNotFoundError
| SessionV2.DestinationNotDirectoryError
| CaptureChangesError
| ApplyChangesError
| ResetSourceChangesError
@@ -86,14 +86,14 @@ const layer = Layer.effect(
const git = yield* Git.Service
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const project = yield* Project.Service
const project = yield* ProjectV2.Service
const sessions = yield* SessionStore.Service
const session = yield* Session.Service
const session = yield* SessionV2.Service
const execution = yield* SessionExecution.Service
const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
const current = yield* sessions.get(input.sessionID)
if (!current) return yield* new Session.NotFoundError({ sessionID: input.sessionID })
if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID })
const value = input.destination.directory.trim()
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))
@@ -173,8 +173,8 @@ export const node = makeGlobalNode({
FSUtil.node,
Git.node,
Global.node,
Project.node,
Session.node,
ProjectV2.node,
SessionV2.node,
SessionStore.node,
SessionExecution.node,
],
@@ -1,17 +1,17 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/sql"
import { Project } from "../project"
import { Workspace } from "../workspace"
import { ProjectV2 } from "../project"
import { WorkspaceV2 } from "../workspace"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().$type<Workspace.ID>().primaryKey(),
id: text().$type<WorkspaceV2.ID>().primaryKey(),
type: text().notNull(),
name: text().notNull().default(""),
branch: text(),
directory: text(),
extra: text({ mode: "json" }),
project_id: text()
.$type<Project.ID>()
.$type<ProjectV2.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
time_used: integer()
+1 -1
View File
@@ -46,7 +46,7 @@ export interface Interface {
readonly remove: (id: ID) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Credential") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
const layer = Layer.effect(
Service,
+1 -1
View File
@@ -20,7 +20,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/storage/Database") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
const databaseLayer = Layer.effect(
Service,
+4 -4
View File
@@ -2,7 +2,7 @@ export * as EventLogger from "./event-logger"
import { Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Bus } from "./bus"
import { EventV2 } from "./event"
const Types = new Set([
"agent.updated",
@@ -13,12 +13,12 @@ const Types = new Set([
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const bus = yield* Bus.Service
const unsubscribe = yield* bus.listen((event) =>
const events = yield* EventV2.Service
const unsubscribe = yield* events.listen((event) =>
Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void,
)
yield* Effect.addFinalizer(() => unsubscribe)
}),
)
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] })
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [EventV2.node] })
@@ -1,7 +1,8 @@
export * as Bus from "./bus"
export * as EventV2 from "./event"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
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 { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm"
import { Database } from "./database/database"
@@ -11,10 +12,18 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export type Subscriber<D extends Event.Definition = Event.Definition> = (event: Event.Payload<D>) => Effect.Effect<void>
export const ID = Event.ID
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 const latestSequence = Effect.fn("Bus.latestSequence")(function* (
export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
db: Database.Interface["db"],
aggregateID: string,
) {
@@ -27,7 +36,7 @@ export const latestSequence = Effect.fn("Bus.latestSequence")(function* (
return row?.seq ?? -1
})
export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* (
export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
db: Database.Interface["db"],
aggregateID: string,
seq: number,
@@ -44,7 +53,7 @@ export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* (
})
export type SerializedEvent = {
readonly id: Event.ID
readonly id: ID
readonly type: string
readonly created?: DateTime.Utc
readonly seq: number
@@ -53,7 +62,7 @@ export type SerializedEvent = {
}
export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()(
"Bus.InvalidDurableEvent",
"EventV2.InvalidDurableEvent",
{
type: Schema.String,
message: Schema.String,
@@ -62,11 +71,11 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
const envelope = (aggregateID: string, seq: number, version: number) => ({
aggregateID,
seq: Event.Seq.make(seq),
version: Event.Version.make(version),
seq: Seq.make(seq),
version: Version.make(version),
})
const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => {
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
const definition = Durable.get(event.type)
if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
@@ -85,7 +94,7 @@ export const durable = Event.durable
export const ephemeral = Event.ephemeral
export interface PublishOptions {
readonly id?: Event.ID
readonly id?: ID
readonly metadata?: Record<string, unknown>
readonly location?: Location.Ref
/** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
@@ -93,13 +102,13 @@ export interface PublishOptions {
}
/** Marker/event union emitted by `log`. */
export type LogItem = Event.Payload | EventLog.Synced
export type LogItem = Payload | EventLog.Synced
export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced"
export type SubscribePayload<D extends readonly Event.Definition[]> = D[number] extends infer Item
? Item extends Event.Definition
? Event.Payload<Item>
export type SubscribePayload<D extends readonly Definition[]> = D[number] extends infer Item
? Item extends Definition
? Payload<Item>
: never
: never
@@ -108,19 +117,19 @@ export interface Subscribe {
* Volatile live channel: every event published from now on, nothing before or
* across a disconnect. Consumers that need reliability combine it with `log`.
*/
(): Stream.Stream<Event.Payload>
<D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
<const D extends readonly [Event.Definition, ...Event.Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
(): Stream.Stream<Payload>
<D extends Definition>(definition: D): Stream.Stream<Payload<D>>
<const D extends readonly [Definition, ...Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
}
const isDefinition = (input: Event.Definition | readonly Event.Definition[]): input is Event.Definition => !Array.isArray(input)
const isDefinition = (input: Definition | readonly Definition[]): input is Definition => !Array.isArray(input)
export interface Interface {
readonly publish: <D extends Event.Definition>(
readonly publish: <D extends Definition>(
definition: D,
data: Event.Data<D>,
data: Data<D>,
options?: PublishOptions,
) => Effect.Effect<Event.Payload<D>>
) => Effect.Effect<Payload<D>>
readonly subscribe: Subscribe
/**
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an
@@ -135,10 +144,10 @@ export interface Interface {
readonly follow?: boolean
}) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Event.Seq>>
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>>
/** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly replay: (
event: SerializedEvent,
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
@@ -151,8 +160,7 @@ export interface Interface {
readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
export interface LayerOptions {
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
@@ -165,20 +173,20 @@ export const layerWith = (options?: LayerOptions) =>
Service,
Effect.gen(function* () {
const pubsub = {
live: yield* PubSub.unbounded<Event.Payload>(),
live: yield* PubSub.unbounded<Payload>(),
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
typed: new Map<string, PubSub.PubSub<Event.Payload>>(),
typed: new Map<string, PubSub.PubSub<Payload>>(),
}
const projectors = new Map<string, Subscriber[]>()
const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service
const logReadPageSize = options?.logReadPageSize ?? 512
const getOrCreate = (definition: Event.Definition) =>
const getOrCreate = (definition: Definition) =>
Effect.gen(function* () {
const existing = pubsub.typed.get(definition.type)
if (existing) return existing
const created = yield* PubSub.unbounded<Event.Payload>()
const created = yield* PubSub.unbounded<Payload>()
pubsub.typed.set(definition.type, created)
return created
})
@@ -196,8 +204,8 @@ export const layerWith = (options?: LayerOptions) =>
)
function commitDurableEvent(
definition: Event.Definition,
event: Event.Payload,
definition: Definition,
event: Payload,
input?: {
readonly seq: number
readonly aggregateID: string
@@ -310,7 +318,7 @@ export const layerWith = (options?: LayerOptions) =>
const committed = {
...event,
durable: { aggregateID, seq, version: durable.version },
} as Event.Payload
} as Payload
for (const projector of list) {
yield* projector(committed)
}
@@ -361,7 +369,7 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publishEvent<D extends Event.Definition>(definition: D, event: Event.Payload<D>, commit?: PublishOptions["commit"]) {
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () {
if (!definition?.durable && commit)
return yield* Effect.die(
@@ -371,22 +379,22 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
if (committed) {
event = {
...event,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
}
yield* notify(event as Event.Payload, true)
yield* notify(event as Payload, true)
return event
}
}
yield* notify(event as Event.Payload, false)
yield* notify(event as Payload, false)
return event
})
}
const observe = (event: Event.Payload, observer: (event: Event.Payload) => Effect.Effect<void>) =>
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
Effect.suspend(() => observer(event)).pipe(
Effect.catchCauseIf(
(cause) => !Cause.hasInterrupts(cause),
@@ -394,7 +402,7 @@ export const layerWith = (options?: LayerOptions) =>
),
)
function notify(event: Event.Payload, isolateListeners: boolean) {
function notify(event: Payload, isolateListeners: boolean) {
return Effect.gen(function* () {
yield* Effect.forEach(
listeners,
@@ -407,7 +415,7 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publish<D extends Event.Definition>(definition: D, data: Event.Data<D>, options?: PublishOptions) {
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location =
@@ -418,13 +426,13 @@ export const layerWith = (options?: LayerOptions) =>
return yield* publishEvent(
definition,
{
id: options?.id ?? Event.ID.create(),
id: options?.id ?? ID.create(),
created: yield* DateTime.now,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
data,
} as Event.Payload<D>,
} as Payload<D>,
options?.commit,
)
})
@@ -446,7 +454,7 @@ export const layerWith = (options?: LayerOptions) =>
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Event.Payload
} as Payload
const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq,
aggregateID: event.aggregateID,
@@ -508,7 +516,7 @@ export const layerWith = (options?: LayerOptions) =>
.pipe(Effect.orDie)
}
const local = <A extends Event.Payload>(stream: Stream.Stream<A>) =>
const local = <A extends Payload>(stream: Stream.Stream<A>) =>
Stream.unwrap(
Effect.serviceOption(Location.Service).pipe(
Effect.map((location) =>
@@ -528,12 +536,12 @@ export const layerWith = (options?: LayerOptions) =>
),
)
function subscribe(): Stream.Stream<Event.Payload>
function subscribe<D extends Event.Definition>(definition: D): Stream.Stream<Event.Payload<D>>
function subscribe<const D extends readonly [Event.Definition, ...Event.Definition[]]>(
function subscribe(): Stream.Stream<Payload>
function subscribe<D extends Definition>(definition: D): Stream.Stream<Payload<D>>
function subscribe<const D extends readonly [Definition, ...Definition[]]>(
definitions: D,
): Stream.Stream<SubscribePayload<D>>
function subscribe(input?: Event.Definition | readonly Event.Definition[]): Stream.Stream<Event.Payload> {
function subscribe(input?: Definition | readonly Definition[]): Stream.Stream<Payload> {
if (input === undefined) return streamLive()
if (isDefinition(input)) {
return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))))
@@ -542,7 +550,7 @@ export const layerWith = (options?: LayerOptions) =>
return streamLive().pipe(Stream.filter((event) => types.has(event.type)))
}
const streamLive = (): Stream.Stream<Event.Payload> => local(Stream.fromPubSub(pubsub.live))
const streamLive = (): Stream.Stream<Payload> => local(Stream.fromPubSub(pubsub.live))
const readAfter = (
aggregateID: string,
@@ -616,7 +624,7 @@ export const layerWith = (options?: LayerOptions) =>
Stream.unwrap(
Effect.gen(function* () {
let sequence = input.after ?? -1
const readThrough = (through: number): Stream.Stream<Event.Payload> =>
const readThrough = (through: number): Stream.Stream<Payload> =>
Stream.paginate(sequence, (cursor) =>
readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe(
Effect.tap((page) =>
@@ -640,7 +648,7 @@ export const layerWith = (options?: LayerOptions) =>
const marker: EventLog.Synced = {
type: "log.synced",
aggregateID: input.aggregateID,
...(target >= 0 ? { seq: Event.Seq.make(target) } : {}),
...(target >= 0 ? { seq: Seq.make(target) } : {}),
}
const replay: Stream.Stream<LogItem> = readThrough(target).pipe(
Stream.map((event): LogItem => event),
@@ -657,7 +665,7 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Event.Seq>> => {
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Seq>> => {
if (aggregateIDs.length === 0) return Effect.succeed(new Map())
return db
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
@@ -666,7 +674,7 @@ export const layerWith = (options?: LayerOptions) =>
.all()
.pipe(
Effect.orDie,
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))),
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))),
)
}
@@ -679,11 +687,11 @@ export const layerWith = (options?: LayerOptions) =>
})
})
const project = <D extends Event.Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
const project = <D extends Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
Effect.sync(() => {
const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type
const list = projectors.get(key) ?? []
list.push((event) => projector(event as Event.Payload<D>))
list.push((event) => projector(event as Payload<D>))
projectors.set(key, list)
})
+2 -2
View File
@@ -1,5 +1,5 @@
import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core"
import { Event } from "@opencode-ai/schema/event"
import type { EventV2 } from "../event"
export const EventSequenceTable = sqliteTable("event_sequence", {
aggregate_id: text().notNull().primaryKey(),
@@ -10,7 +10,7 @@ export const EventSequenceTable = sqliteTable("event_sequence", {
export const EventTable = sqliteTable(
"event",
{
id: text().$type<Event.ID>().primaryKey(),
id: text().$type<EventV2.ID>().primaryKey(),
aggregate_id: text()
.notNull()
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
+6 -6
View File
@@ -64,7 +64,7 @@ export interface Interface {
readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {}
/**
* 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] })
/**
* Deferred until the corresponding integrations exist.
* Deferred until the corresponding V2 integrations exist.
*/
// TODO: Add formatter integration after formatter runtime exists.
// TODO: Publish watcher/file-edit events after watcher integration exists.
// TODO: Add snapshots / undo after snapshot design exists.
// TODO: Notify LSP and collect diagnostics after LSP runtime exists.
// TODO: Add formatter integration after V2 formatter runtime exists.
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
+1 -1
View File
@@ -56,7 +56,7 @@ export interface Interface {
readonly grep: (input: GrepInput) => Effect.Effect<readonly Match[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem") {}
const baseLayer = Layer.effect(
Service,
@@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os"
import path from "path"
import { Config } from "../config"
import { Bus } from "../bus"
import { EventV2 } from "../event"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git"
import { Location } from "../location"
@@ -30,12 +30,12 @@ const layer = Layer.effect(
Effect.gen(function* () {
const location = yield* Location.Service
const watcher = yield* Watcher.Service
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const configService = yield* Config.Service
const publish = (update: { type: "create" | "update" | "delete"; path: string }) =>
bus.publish(FileSystem.Event.Changed, {
events.publish(FileSystem.Event.Changed, {
file: update.path,
event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink",
})
@@ -86,5 +86,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node],
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
})
+1 -1
View File
@@ -22,7 +22,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem/Search") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {}
export const ripgrepLayer = Layer.effect(
Service,
+7 -7
View File
@@ -3,7 +3,7 @@ export * as Form from "./form"
import { Form } from "@opencode-ai/schema/form"
import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Bus } from "./bus"
import { EventV2 } from "./event"
const RETENTION = Duration.minutes(10)
@@ -32,7 +32,7 @@ export type Answer = typeof Answer.Type
export const Reply = Form.Reply
export type Reply = typeof Reply.Type
export { Event } from "@opencode-ai/schema/form"
export const Event = Form.Event
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", {
id: ID,
@@ -99,7 +99,7 @@ interface Entry {
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const forms = yield* Cache.makeWith<ID, Entry>(
() => 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>(),
}
yield* Cache.set(forms, id, entry)
yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
return form
}),
),
@@ -183,7 +183,7 @@ export const layer = Layer.effect(
const invalid = validateAnswer(entry.form, input.answer)
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
const next: TerminalState = { status: "answered", answer: input.answer }
yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
@@ -196,7 +196,7 @@ export const layer = Layer.effect(
const entry = yield* find(id)
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id })
const next: TerminalState = { status: "cancelled" }
yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* Cache.set(forms, id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
@@ -221,7 +221,7 @@ export const layer = Layer.effect(
export const locationLayer = layer
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
function validateAnswer(form: Info, answer: Answer) {
const fields = new Map(form.fields.map((field) => [field.key, field] as const))
+3 -3
View File
@@ -5,11 +5,11 @@ import { Context, Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { llmClient } from "./effect/app-node-platform"
import { ModelResolver } from "./model-resolver"
import { Model } from "./model"
import { ModelV2 } from "./model"
export interface TextInput {
readonly prompt: string
readonly model?: Model.Ref
readonly model?: ModelV2.Ref
}
export class ModelSelectionError extends Schema.TaggedErrorClass<ModelSelectionError>()(
@@ -28,7 +28,7 @@ export interface Interface {
readonly text: (input: TextInput) => Effect.Effect<string, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Generate") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Generate") {}
export const layer = Layer.effect(
Service,
+1 -1
View File
@@ -189,7 +189,7 @@ export interface Interface {
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
const layer = Layer.effect(
Service,
+17 -17
View File
@@ -2,8 +2,8 @@ export * as CopilotModels from "./models"
import { Money } from "@opencode-ai/schema/money"
import { Option, Schema } from "effect"
import { Model } from "../model"
import { Provider } from "../provider"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
const RemoteModel = Schema.Struct({
model_picker_enabled: Schema.Boolean,
@@ -70,7 +70,7 @@ type UsableModel = RemoteModel & {
}
}
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly Model.Info[]) {
export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) {
const response = await fetch(`${baseURL}/models`, {
headers,
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) {
const key = Model.ID.make(id)
const key = ModelV2.ID.make(id)
if (result.has(key)) continue
result.set(key, build(key, model, baseURL))
}
@@ -114,7 +114,7 @@ function usable(model: RemoteModel): model is UsableModel {
)
}
function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Model.Info) {
function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) {
const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false
const endpoint = messages
? "messages"
@@ -134,15 +134,15 @@ function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Mo
: remote.version
const released = previous?.time.released || Date.parse(version)
return Model.Info.make({
...Model.Info.default(Provider.ID.githubCopilot, id),
return ModelV2.Info.make({
...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id),
id,
modelID: Model.ID.make(remote.id),
providerID: Provider.ID.githubCopilot,
family: previous?.family ?? Model.Family.make(remote.capabilities.family),
modelID: ModelV2.ID.make(remote.id),
providerID: ProviderV2.ID.githubCopilot,
family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family),
name: previous?.name ?? remote.name,
package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: Provider.mergeOverlay(previous?.settings, {
package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: ProviderV2.mergeOverlay(previous?.settings, {
baseURL: messages ? `${baseURL}/v1` : baseURL,
...(endpoint ? { endpoint } : {}),
}),
@@ -175,11 +175,11 @@ function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Mo
})
}
function variants(remote: UsableModel, messages: boolean): Model.Info["variants"] {
function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] {
const efforts = remote.capabilities.supports.reasoning_effort ?? []
if (!messages && efforts.length) {
return efforts.map((effort) => ({
id: Model.VariantID.make(effort),
id: ModelV2.VariantID.make(effort),
settings: {
reasoningEffort: effort,
reasoningSummary: "auto",
@@ -189,7 +189,7 @@ function variants(remote: UsableModel, messages: boolean): Model.Info["variants"
}
if (efforts.length && remote.capabilities.supports.adaptive_thinking) {
return efforts.map((effort) => ({
id: Model.VariantID.make(effort),
id: ModelV2.VariantID.make(effort),
settings: {
thinking: {
type: "adaptive",
@@ -203,11 +203,11 @@ function variants(remote: UsableModel, messages: boolean): Model.Info["variants"
if (max === undefined) return []
return [
{
id: Model.VariantID.make("max"),
id: ModelV2.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: max - 1 } },
},
{
id: Model.VariantID.make("high"),
id: ModelV2.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } },
},
]
+1 -1
View File
@@ -26,7 +26,7 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionDiscovery") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionDiscovery") {}
export const layer = (options?: Options) => Layer.effect(
Service,
+1 -1
View File
@@ -10,7 +10,7 @@ export interface Interface {
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionBuiltIns") {}
const layer = Layer.effect(
Service,
+16 -16
View File
@@ -20,7 +20,7 @@ import {
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { State } from "./state"
import { Bus } from "./bus"
import { EventV2 } from "./event"
import { IntegrationConnection } from "./integration/connection"
import { AppProcess } from "@opencode-ai/util/process"
import { ChildProcess } from "effect/unstable/process"
@@ -129,7 +129,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError
export { Event } from "@opencode-ai/schema/integration"
export const Event = Integration.Event
export const 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/Integration") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
const terminalRetention = Duration.toMillis(Duration.minutes(1))
@@ -271,7 +271,7 @@ const layer = Layer.effect(
Service,
Effect.gen(function* () {
const credentials = yield* Credential.Service
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const processes = yield* AppProcess.Service
const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
@@ -338,7 +338,7 @@ const layer = Layer.effect(
},
},
}),
finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
})
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.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
}).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* close(attempt.scope)
if (Exit.isFailure(persistence)) return
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
}),
)
})
@@ -706,24 +706,24 @@ const layer = Layer.effect(
label: input.label,
value: Credential.Key.make({ type: "key", key: input.key }),
})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {})
}),
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
const credential = yield* credentials.get(credentialID)
yield* credentials.update(credentialID, updates)
if (credential) {
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* bus.publish(Integration.Event.Updated, {})
yield* events.publish(Event.Updated, {})
}),
remove: Effect.fn("Integration.connection.remove")(function* (credentialID) {
const credential = yield* credentials.get(credentialID)
yield* credentials.remove(credentialID)
if (credential) {
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* bus.publish(Integration.Event.Updated, {})
yield* events.publish(Event.Updated, {})
}),
},
oauth: {
@@ -809,5 +809,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Credential.node, Bus.node, AppProcess.node],
deps: [Credential.node, EventV2.node, AppProcess.node],
})
+1 -1
View File
@@ -14,7 +14,7 @@ export interface Interface {
readonly remove: (key: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/KV") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {}
const layer = Layer.effect(
Service,
+1 -1
View File
@@ -60,7 +60,7 @@ export interface Interface {
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationMutation") {}
interface ResolvedPath {
readonly canonical: string
+20 -15
View File
@@ -1,12 +1,13 @@
import { Effect, Layer, LayerMap } from "effect"
import { Agent } from "./agent"
import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { Command } from "./command"
import { CodeMode } from "./codemode"
import { CommandV2 } from "./command"
import { Config } from "./config"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Node } from "@opencode-ai/util/effect/app-node"
import { Bus } from "./bus"
import { EventV2 } from "./event"
import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
@@ -20,12 +21,12 @@ import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map"
import { ModelResolver } from "./model-resolver"
import { MCP } from "./mcp/index"
import { Permission } from "./permission"
import { Plugin } from "./plugin"
import { PermissionV2 } from "./permission"
import { PluginV2 } from "./plugin"
import { PluginSupervisor } from "./plugin/supervisor"
import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty"
import { Question } from "./question"
import { QuestionV2 } from "./question"
import { Shell } from "./shell"
import { Reference } from "./reference"
import { WebSearch } from "./websearch"
@@ -34,7 +35,7 @@ import { SessionRunnerLLM } from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction"
import { SessionTitle } from "./session/title"
import { Skill } from "./skill"
import { SkillV2 } from "./skill"
import { SkillInstructions } from "./skill/instructions"
import { Snapshot } from "./snapshot"
import { InstructionDiscovery } from "./instruction-discovery"
@@ -44,7 +45,8 @@ import { SessionInstructions } from "./session/instructions"
import { SessionGenerateNode } from "./session/generate-node"
import { McpTool } from "./tool/mcp"
import { ReadToolFileSystem } from "./tool/read-filesystem"
import { Tool } from "./tool"
import { ToolRegistry } from "./tool/registry"
import { ToolOutputStore } from "./tool-output-store"
import { Vcs } from "./vcs"
export { LocationServiceMap } from "./location-service-map"
@@ -52,15 +54,15 @@ export { LocationServiceMap } from "./location-service-map"
const locationServiceNodes = [
Location.node,
Config.node,
Agent.node,
Command.node,
AgentV2.node,
CommandV2.node,
Reference.node,
WebSearch.node,
Integration.node,
Catalog.node,
ModelResolver.node,
AISDK.node,
Plugin.node,
PluginV2.node,
PluginSupervisor.node,
ProjectCopy.node,
ProjectCopy.refreshNode,
@@ -68,20 +70,23 @@ const locationServiceNodes = [
FileSystem.node,
Pty.node,
Shell.node,
Skill.node,
SkillV2.node,
CodeMode.node,
InstructionBuiltIns.node,
InstructionDiscovery.node,
LocationMutation.node,
FileMutation.node,
MCP.node,
Permission.node,
Tool.node,
PermissionV2.node,
ToolOutputStore.node,
ToolRegistry.node,
ToolRegistry.toolsNode,
Image.node,
SkillInstructions.node,
ReferenceInstructions.node,
InstructionEntry.node,
Form.node,
Question.node,
QuestionV2.node,
Generate.node,
SessionGenerateNode.node,
ReadToolFileSystem.node,
+24 -24
View File
@@ -9,7 +9,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Config } from "../config"
import { ConfigMCP } from "../config/mcp"
import { Credential } from "../credential"
import { Bus } from "../bus"
import { EventV2 } from "../event"
import { Form } from "../form"
import { Integration } from "../integration"
import { IntegrationConnection } from "../integration/connection"
@@ -155,7 +155,7 @@ export interface Interface {
}) => Effect.Effect<ResourceContent | undefined, NotFoundError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {}
export const Options = Schema.Struct({
clientInfo: Schema.optional(
@@ -172,7 +172,7 @@ export const layer = (options?: Options) => Layer.effect(
Effect.gen(function* () {
const config = yield* Config.Service
const location = yield* Location.Service
const bus = yield* Bus.Service
const events = yield* EventV2.Service
const forms = yield* Form.Service
const integration = yield* Integration.Service
const credentials = yield* Credential.Service
@@ -433,9 +433,9 @@ export const layer = (options?: Options) => Layer.effect(
Effect.map((defs) => {
entry.prompts = defs.map((def) => toPrompt(name, def))
}),
Effect.andThen(bus.publish(Command.Event.Updated, {})),
Effect.andThen(events.publish(Command.Event.Updated, {})),
Effect.catch(() =>
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))),
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))),
),
)
@@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}),
),
)
@@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect(
connection.onToolsChanged(() =>
live(
refreshTools(name, entry, connection).pipe(
Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })),
Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })),
),
),
)
connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection)))
connection.onResourcesChanged(() => live(bus.publish(McpEvent.ResourcesChanged, { server: name })))
connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name })))
}
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
// disabled/failed status for the duration of the connection attempt.
entry.status = { status: "pending" }
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
const scope = yield* Scope.fork(root)
entry.scope = scope
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
// after the initial registration sweep and emits no list-changed notification would otherwise
// stay invisible to the model.
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection))
return
}
@@ -544,7 +544,7 @@ export const layer = (options?: Options) => Layer.effect(
? { status: "needs_auth" }
: { status: "failed", error: error instanceof Error ? error.message : String(error) }
yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status })
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined)))
const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) {
@@ -555,9 +555,9 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined
entry.prompts = undefined
yield* Scope.close(scope, Exit.void)
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
})
// 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))
})
fork(
bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
events.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => owned.has(event.data.integrationID)),
Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))),
Effect.ignore,
@@ -632,7 +632,7 @@ export const layer = (options?: Options) => Layer.effect(
yield* register(name, entry)
if (config.disabled) {
entry.status = { status: "disabled" }
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
return
}
yield* startServer(name, entry)
@@ -657,7 +657,7 @@ export const layer = (options?: Options) => Layer.effect(
const target = yield* requireServer(name)
yield* stopServer(name, target.entry)
target.entry.status = { status: "disabled" }
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name))
}),
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
// reuses them without forcing re-auth, matching add()'s replacement semantics.
runtime.delete(name)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(locks.withLock(name))
}),
tools: Effect.fn("MCP.tools")(function* () {
@@ -793,7 +793,7 @@ export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
})
}
+6 -6
View File
@@ -2,8 +2,8 @@ export * as McpInstructions from "./instructions"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema } from "effect"
import { Agent } from "../agent"
import { Permission } from "../permission"
import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
import { McpTool } from "../tool/mcp"
import { MCP } from "./index"
import { Instructions } from "../instructions/index"
@@ -55,10 +55,10 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
}
export interface Interface {
readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions>
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/McpInstructions") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpInstructions") {}
export const layer = Layer.effect(
Service,
@@ -83,7 +83,7 @@ export const layer = Layer.effect(
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded",
})
const canExecute = Permission.evaluate("execute", "*", agent.permissions).effect !== "deny"
const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny"
// Instructions are useful only when this agent can reach at least one server tool.
const visible = instructions
.flatMap((item) => {
@@ -93,7 +93,7 @@ export const layer = Layer.effect(
if (
!owned.some(
(tool) =>
Permission.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
)
)
return []
+48 -48
View File
@@ -15,17 +15,17 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { Credential } from "./credential"
import { Integration } from "./integration"
import { Capabilities, ID, Info, Ref, VariantID } from "./model"
import { ModelV2 } from "./model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex"
import { Provider } from "./provider"
import { ProviderV2 } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError",
{
providerID: Provider.ID,
modelID: ID,
variant: VariantID,
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: ModelV2.VariantID,
},
) {
override get message() {
@@ -36,8 +36,8 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: Provider.ID,
modelID: ID,
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
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. */
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: Ref
readonly ref: ModelV2.Ref
/** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: Capabilities
readonly capabilities: ModelV2.Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: Info["cost"]
readonly cost: ModelV2.Info["cost"]
}
export interface Interface {
readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {}
const apiKey = (model: Info, credential?: Credential.Value) => {
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey
@@ -74,7 +74,7 @@ const apiKey = (model: Info, credential?: Credential.Value) => {
return undefined
}
const withDefaults = (model: Info, route: AnyRoute) =>
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
route.with({
provider: model.providerID,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
@@ -84,8 +84,8 @@ const withDefaults = (model: Info, route: AnyRoute) =>
limits: { context: model.limit.context, output: model.limit.output },
})
const providerHeaders = (model: Info) => {
const packageName = Provider.packageName(model.package)
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization)
@@ -93,16 +93,16 @@ const providerHeaders = (model: Info) => {
generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
}
const providerOptions = (
model: Info,
model: ModelV2.Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined
const packageName = Provider.packageName(model.package)
const packageName = ProviderV2.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
@@ -110,9 +110,9 @@ const providerOptions = (
}
export const withVariant = (
model: Info,
variantID: VariantID | undefined,
): Effect.Effect<Info, VariantUnavailableError> => {
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
@@ -126,37 +126,37 @@ export const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
draft.settings = Provider.mergeOverlay(draft.settings, variant.settings)
draft.headers = Provider.mergeHeaders(draft.headers, variant.headers)
draft.body = Provider.mergeOverlay(draft.body, variant.body)
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = (
model: Info,
model: ModelV2.Info,
credential?: Credential.Value,
dependencies?: Dependencies,
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = produce(model, (draft) => {
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
if (credential?.type === "key" && credential.metadata !== undefined)
draft.body = Provider.mergeOverlay(draft.body, credential.metadata)
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
})
const packageName = Provider.packageName(resolved.package)
const packageName = ProviderV2.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
@@ -164,7 +164,7 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
@@ -172,7 +172,7 @@ export const fromCatalogModel = (
)
}
if (
Provider.isAISDK(resolved.package) &&
ProviderV2.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
@@ -182,10 +182,10 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (Provider.isAISDK(resolved.package)) {
if (ProviderV2.isAISDK(resolved.package)) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = Provider.mergeOverlay(draft.settings, {
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
@@ -197,7 +197,7 @@ export const fromCatalogModel = (
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe(
const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)),
)
const configured = { ...resolved.settings, ...credential?.metadata }
@@ -249,7 +249,7 @@ const withoutNativeAuthSettings = (settings: Record<string, unknown>) => {
}
const codexModel = (
model: Info,
model: ModelV2.Info,
credential: Credential.Value | undefined,
key: ReturnType<typeof Auth.value> | undefined,
) => {
@@ -264,7 +264,7 @@ const codexModel = (
.model({ id: model.modelID ?? model.id, compatibility: model.compatibility })
}
const unsupported = (model: Info) =>
const unsupported = (model: ModelV2.Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
@@ -272,13 +272,13 @@ const unsupported = (model: Info) =>
})
export const resolveModel = (
model: Info,
variant: VariantID | undefined,
model: ModelV2.Info,
variant: ModelV2.VariantID | undefined,
credential?: Credential.Value,
dependencies?: Dependencies,
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const supported = (model: Info) => Boolean(model.package)
export const supported = (model: ModelV2.Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */
export const layer = Layer.effect(
@@ -289,8 +289,8 @@ export const layer = Layer.effect(
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
const load = Effect.fn("ModelResolver.resolveModel")(function* (
selected: Info,
variant?: VariantID,
selected: ModelV2.Info,
variant?: ModelV2.VariantID,
) {
const provider = yield* catalog.provider.get(selected.providerID)
const connection = yield* integrations.connection.active(
@@ -301,13 +301,13 @@ export const layer = Layer.effect(
variant,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
return {
model,
ref: Ref.make({
ref: ModelV2.Ref.make({
id: selected.id,
providerID: selected.providerID,
...(variant === undefined ? {} : { variant }),

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