refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
parent 0fd73a2976
commit 8db7487c89
466 changed files with 9380 additions and 11046 deletions

View file

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

View file

@ -36,9 +36,9 @@
"packages/slack"
],
"catalog": {
"@effect/opentelemetry": "4.0.0-beta.98",
"@effect/platform-node": "4.0.0-beta.98",
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
"@effect/opentelemetry": "4.0.0-beta.101",
"@effect/platform-node": "4.0.0-beta.101",
"@effect/sql-sqlite-bun": "4.0.0-beta.101",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6",
@ -50,6 +50,7 @@
"@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",
@ -68,7 +69,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.98",
"effect": "4.0.0-beta.101",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"hono": "4.10.7",
@ -151,7 +152,8 @@
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:"
"@types/node": "catalog:",
"effect": "catalog:"
},
"patchedDependencies": {
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
@ -165,7 +167,7 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@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.98": "patches/effect@4.0.0-beta.98.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
}
}

View file

@ -1,4 +1,5 @@
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"
@ -19,7 +20,6 @@ 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: ToolContent,
item: Tool.Content,
) {
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<ToolContent> = part.result.value
const content: ReadonlyArray<Tool.Content> = part.result.value
return yield* Effect.forEach(content, lowerToolResultContentItem)
})

View file

@ -1,4 +1,5 @@
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"
@ -16,7 +17,6 @@ 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<ToolContent> = part.result.value
const content: ReadonlyArray<Tool.Content> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text)
const media: GeminiInlineDataPart[] = []
for (const item of content) {

View file

@ -1,4 +1,5 @@
import { Effect, Schema } from "effect"
import type { Content } from "@opencode-ai/schema/tool"
import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol"
import {
@ -14,7 +15,6 @@ 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: ToolContent,
item: Content,
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<ToolContent> = part.result.value
const content: ReadonlyArray<Content> = 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<ToolContent> = part.result.value
const content: ReadonlyArray<Content> = part.result.value
input.push({
role: "user",
content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)),

View file

@ -1,4 +1,5 @@
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,7 +18,6 @@ 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<ToolContent> = part.result.value
const content: ReadonlyArray<Tool.Content> = 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")

View file

@ -1,4 +1,5 @@
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"
@ -9,7 +10,6 @@ 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: ToolFileContent, supportedMimes: ReadonlySet<string>) =>
export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")

View file

@ -1,4 +1,5 @@
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")
@ -152,8 +153,4 @@ export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
* Anything thrown or yielded by a handler that is not a `ToolFailure` is
* treated as a defect and fails the stream.
*/
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)),
}) {}
export class ToolFailure extends Tool.Error {}

View file

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

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("LLM.ToolFailure", (failure) =>
Effect.catchTag("Tool.Error", (failure) =>
Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)),
),
)

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<ToolContent>
) => ReadonlyArray<Tool.Content>
/**
* 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<ToolContent>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
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<ToolContent>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
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<ToolContent>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<Tool.Content>
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<ToolContent>) | undefined,
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<Tool.Content>) | undefined,
toStructuredOutput: ((output: unknown) => unknown) | undefined,
parameters: unknown,
callID: ToolCallPart["id"],

View file

@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import { Content } from "@opencode-ai/schema/tool"
import { Effect, Schema, Stream } from "effect"
import {
GenerationOptions,
@ -7,7 +8,6 @@ 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(ToolContent)
const decode = Schema.decodeUnknownSync(Content)
expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({
type: "file",

View file

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

View file

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

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.v2.asked" && event.data.sessionID === input.sessionID) {
if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) {
const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined
await replyPermission({
client: input.client,

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.v2.asked" }>
type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.asked" }>
type Connection = Pick<AgentSideConnection, "requestPermission"> & Partial<Pick<AgentSideConnection, "writeTextFile">>
type Tool = { readonly name: string; readonly input: ToolInput }

View file

@ -9,8 +9,8 @@ import {
Provider,
Reference,
Skill,
} from "@opencode-ai/plugin/v2/effect"
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
} from "@opencode-ai/plugin/effect"
import { Tool } from "@opencode-ai/schema/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,
Tool: { Error: Tool.Error },
}

View file

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

View file

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

View file

@ -13,11 +13,13 @@ 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: process.cwd(),
cwd: startupDirectory,
// Explicit entry wins over anything inherited, so a user-exported
// OPENCODE_PASSWORD cannot shadow the child's lease credential.
env: { OPENCODE_PASSWORD: password },

View file

@ -1,11 +1,13 @@
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 (!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]]
if (!entrypoint) throw new Error("Failed to resolve CLI entrypoint")
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), entrypoint]
return [process.execPath, entrypoint]
}
function nodeFlags() {

View file

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

View file

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

View file

@ -75,20 +75,11 @@ export const define = sdk.Plugin.define`
const effectPluginModule = promisePluginModule
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
.replace("Promise plugin", "Effect plugin")
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 promiseToolModule = `export {}`
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
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`
export const Error = sdk.Tool.Error
`
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"
@ -100,17 +91,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/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",
"@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",
})}
const __ocPluginSources = ${JSON.stringify({
"opencode:plugin-v2": promiseModule,
"opencode:plugin-v2-plugin": promisePluginModule,
"opencode:plugin-v2-tool": promiseToolModule,
"opencode:plugin-promise-plugin": promisePluginModule,
"opencode:plugin-promise-tool": promiseToolModule,
"opencode:plugin-v2-effect": effectModule,
"opencode:plugin-v2-effect-plugin": effectPluginModule,
"opencode:plugin-v2-effect-tool": effectToolModule,

View file

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

View file

@ -6,11 +6,86 @@ import {
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Effect } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command"
import { Credential } from "@opencode-ai/schema/credential"
import { Event } from "@opencode-ai/schema/event"
import { EventLog } from "@opencode-ai/schema/event-log"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Form } from "@opencode-ai/schema/form"
import { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
import { Mcp } from "@opencode-ai/schema/mcp"
import { Model } from "@opencode-ai/schema/model"
import { Permission } from "@opencode-ai/schema/permission"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Project } from "@opencode-ai/schema/project"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
import { AgentAttachment, FileAttachment, Prompt, PromptMention } from "@opencode-ai/schema/prompt"
import { PromptInput } from "@opencode-ai/schema/prompt-input"
import { Provider } from "@opencode-ai/schema/provider"
import { Pty } from "@opencode-ai/schema/pty"
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
import { Question } from "@opencode-ai/schema/question"
import { Reference } from "@opencode-ai/schema/reference"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
import { Session } from "@opencode-ai/schema/session"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { SessionPending } from "@opencode-ai/schema/session-pending"
import { Shell } from "@opencode-ai/schema/shell"
import { Skill } from "@opencode-ai/schema/skill"
import { Vcs } from "@opencode-ai/schema/vcs"
import { WebSearch } from "@opencode-ai/schema/websearch"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Effect, Schema } from "effect"
import { fileURLToPath } from "url"
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(
@ -22,14 +97,40 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/promise/generated", import.meta.url)),
),
write(
emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }),
emitEffectImported(effectContract, {
module: "../../contract",
api: "ClientApi",
shapeModule: "../api/api.js",
}),
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
),
write(
emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }),
emitEffectShape(effectContract, {
typeReferences: effectTypeReferences,
outputTypes: {
"event.subscribe": {
name: "OpenCodeEvent",
import: 'import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"',
},
},
}),
fileURLToPath(new URL("../src/effect/api", import.meta.url)),
),
],
{ 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

View file

@ -7,6 +7,8 @@ import type {
LocationGetOutput,
AgentListInput,
AgentListOutput,
AgentGetInput,
AgentGetOutput,
PluginListInput,
PluginListOutput,
SessionListInput,
@ -403,6 +405,18 @@ 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) =>

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

View file

@ -1,5 +1,6 @@
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>
@ -9,6 +10,10 @@ 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<
@ -37,4 +42,4 @@ const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.re
key: "review-notes",
})
void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]

View file

@ -14,6 +14,14 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
expect(Client.Session).toBe(Session)
})
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" })

View file

@ -99,6 +99,7 @@
"@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",

View file

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

View file

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

View file

@ -1,9 +1,9 @@
export * as AgentV2 from "./agent"
export * as Agent from "./agent"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { EventV2 } from "./event"
import { Bus } from "./bus"
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 const Event = Agent.Event
export { Event } from "@opencode-ai/schema/agent"
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/v2/Agent") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.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: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Agent.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("AgentV2.get")(function* (id) {
get: Effect.fn("Agent.get")(function* (id) {
return state.get().agents.get(id)
}),
default: Effect.fn("AgentV2.default")(function* () {
default: Effect.fn("Agent.default")(function* () {
return selectedDefault()
}),
resolve: Effect.fn("AgentV2.resolve")(function* (id) {
resolve: Effect.fn("Agent.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault()
}),
select: Effect.fn("AgentV2.select")(function* (id) {
select: Effect.fn("Agent.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("AgentV2.list")(function* () {
list: Effect.fn("Agent.list")(function* () {
return Array.fromIterable(state.get().agents.values())
}),
})
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })

View file

@ -32,8 +32,8 @@ import {
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route"
import { ProviderShared } from "@opencode-ai/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import type { ID, Info } from "./model"
import { Provider } 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: ModelV2.Info
readonly model: Info
readonly package: string
readonly options: Record<string, any>
sdk?: SDK
}
export interface LanguageEvent {
readonly model: ModelV2.Info
readonly model: 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: ModelV2.Info, pkg: string) {
function prepareOptions(model: Info, pkg: string) {
const projected = mapBodyToProviderOptions(model, pkg)
const options: Record<string, any> = {
name: model.providerID,
@ -146,7 +146,7 @@ function prepareOptions(model: ModelV2.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(ProviderV2.mergeOverlay(decoded, model.body))
opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body))
}
}
@ -162,11 +162,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
}
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID,
providerID: Provider.ID,
cause: Schema.Defect(),
}) {}
function initError(providerID: ProviderV2.ID) {
function initError(providerID: Provider.ID) {
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
}
@ -181,11 +181,11 @@ export interface Interface {
}
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: Info) => Effect.Effect<Model, InitError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
export const locationLayer = Layer.effect(
Service,
@ -260,13 +260,13 @@ export const locationLayer = Layer.effect(
})
const existing = languages.get(key)
if (existing) return existing
if (!ProviderV2.isAISDK(model.package))
if (!Provider.isAISDK(model.package))
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported package ${model.package}`),
})
const packageName = ProviderV2.packageName(model.package)
const packageName = Provider.packageName(model.package)
const options = prepareOptions(model, packageName)
const sdkKey = cacheKey({
providerID: model.providerID,
@ -301,8 +301,8 @@ export const locationLayer = Layer.effect(
export const defaultLayer = locationLayer
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const packageName = ProviderV2.packageName(info.package!)
function modelFromLanguage(info: Info, language: LanguageModelV3) {
const packageName = Provider.packageName(info.package!)
const projected = mapBodyToProviderOptions(info, packageName)
const optionKey = providerOptionKey(packageName, info.providerID)
const providerOptions = (() => {
@ -352,7 +352,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
})
}
function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<string, unknown>>) {
function gatewayProviderOptions(modelID: ID, settings: Readonly<Record<string, unknown>>) {
const gateway =
typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway)
? Object.fromEntries(Object.entries(settings.gateway))
@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly<Record<s
return { gateway: model }
}
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
function providerOptionKey(packageName: string | undefined, providerID: Provider.ID) {
if (packageName === "@ai-sdk/google") return "google"
if (packageName === "@ai-sdk/google-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: ModelV2.Info, packageName: string) {
function mapBodyToProviderOptions(model: 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 ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings
const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings
if (!pro) return { settings: normalized, body: model.body }
const body = { ...model.body }
delete body.reasoning
return {
settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }),
settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }),
body: Object.keys(body).length === 0 ? undefined : body,
}
}

View file

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

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 { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { Model } from "./model"
import { Provider } from "./provider"
import { Bus } from "./bus"
import { State } from "./state"
import { Integration } from "./integration"
export type ProviderRecord = {
provider: ProviderV2.MutableInfo
models: Map<ModelV2.ID, ModelV2.MutableInfo>
provider: Provider.MutableInfo
models: Map<Model.ID, Model.MutableInfo>
}
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID }
export const Event = Catalog.Event
export { Event } from "@opencode-ai/schema/catalog"
type Data = {
providers: Map<ProviderV2.ID, ProviderRecord>
providers: Map<Provider.ID, ProviderRecord>
defaultModel?: DefaultModel
}
export type Draft = {
provider: {
list: () => readonly ProviderRecord[]
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID) => void
get: (providerID: Provider.ID) => ProviderRecord | undefined
update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void
remove: (providerID: Provider.ID) => void
}
model: {
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
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
default: {
get: () => DefaultModel | undefined
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
set: (providerID: Provider.ID, modelID: Model.ID) => void
}
}
}
export interface Interface extends State.Transformable<Draft> {
readonly provider: {
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
readonly all: () => Effect.Effect<ProviderV2.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]>
readonly get: (providerID: Provider.ID) => Effect.Effect<Provider.Info | undefined>
readonly all: () => Effect.Effect<Provider.Info[]>
readonly available: () => Effect.Effect<Provider.Info[]>
}
readonly model: {
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>
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>
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Catalog") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const integrations = yield* Integration.Service
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration
}
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
const projectModel = (model: Model.Info, provider: Provider.Info) => {
return {
...model,
package: model.package ?? provider.package,
settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body),
} satisfies ModelV2.Info
settings: Provider.mergeOverlay(provider.settings, model.settings),
headers: Provider.mergeHeaders(provider.headers, model.headers),
body: Provider.mergeOverlay(provider.body, model.body),
} satisfies Model.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: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, current)
}
@ -110,13 +110,13 @@ const layer = Layer.effect(
let record = draft.providers.get(providerID)
if (!record) {
record = {
provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, record)
}
const model =
record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo)
record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model)
model.id = modelID
@ -135,8 +135,8 @@ const layer = Layer.effect(
}
return result
},
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
yield* events.publish(Event.Updated, {})
finalize: Effect.fn("Catalog.finalize")(function* (catalog) {
yield* bus.publish(Catalog.Event.Updated, {})
}),
})
const result: Interface = {
@ -144,15 +144,15 @@ const layer = Layer.effect(
reload: state.reload,
provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
get: Effect.fn("Catalog.provider.get")(function* (providerID) {
return state.get().providers.get(providerID)?.provider
}),
all: Effect.fn("CatalogV2.provider.all")(function* () {
all: Effect.fn("Catalog.provider.all")(function* () {
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
}),
available: Effect.fn("CatalogV2.provider.available")(function* () {
available: Effect.fn("Catalog.provider.available")(function* () {
const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
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("CatalogV2.model.get")(function* (providerID, modelID) {
get: Effect.fn("Catalog.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("CatalogV2.model.all")(function* () {
all: Effect.fn("Catalog.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("CatalogV2.model.available")(function* () {
available: Effect.fn("Catalog.model.available")(function* () {
const providers = new Set((yield* result.provider.available()).map((provider) => provider.id))
const models: ModelV2.Info[] = []
const models: Model.Info[] = []
for (const record of state.get().providers.values()) {
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("CatalogV2.model.default")(function* () {
default: Effect.fn("Catalog.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("CatalogV2.model.small")(function* (providerID) {
small: Effect.fn("Catalog.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 === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) {
if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) {
return
}
if (providerID === ProviderV2.ID.opencode) {
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
if (providerID === Provider.ID.opencode) {
const gpt5Nano = record.models.get(Model.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: [EventV2.node, Integration.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] })

View file

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

View file

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

View file

@ -1,11 +1,11 @@
export * as CommandV2 from "./command"
export * as Command from "./command"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { Command } from "@opencode-ai/schema/command"
import { State } from "./state"
import { MCP } from "./mcp/index"
import { EventV2 } from "./event"
import { Bus } from "./bus"
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 const Event = Command.Event
export { Event } from "@opencode-ai/schema/command"
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/v2/Command") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = (options?: ShellSelect.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const mcp = yield* MCP.Service
const events = yield* EventV2.Service
const bus = yield* Bus.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: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
})
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const 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("CommandV2.get")(function* (name) {
get: Effect.fn("Command.get")(function* (name) {
const command = staticCommand(name)
if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name)
}),
list: Effect.fn("CommandV2.list")(function* () {
list: Effect.fn("Command.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[]
const 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("CommandV2.evaluate")(function* (input) {
evaluate: Effect.fn("Command.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, EventV2.node, AppProcess.node, Config.node, Location.node],
deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node],
})
}

View file

@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { EventV2 } from "./event"
import { Bus } from "./bus"
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/v2/Config") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/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 events = yield* EventV2.Service
const bus = yield* Bus.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* events.publish(ConfigSchema.Event.Updated, {})
yield* bus.publish(ConfigSchema.Event.Updated, {})
}),
),
)
@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect(
),
Effect.forkScoped({ startImmediately: true }),
)
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.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* events.subscribe(WellKnown.Event.Updated).pipe(
yield* bus.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, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
})
}

View file

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

View file

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

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>("ConfigV2.Command")({
export class Info extends Schema.Class<Info>("Config.Command")({
template: Schema.String,
description: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Option, Schema, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Agent } 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 { PermissionV2 } from "../../permission"
import { Permission } from "../../permission"
import type { LocationMutation } from "../../location-mutation"
import type { ReadTool } from "../../tool/read"
import type { EditTool } from "../../tool/edit"
import type { ReadTool } from "../../tool/plugin/read"
import type { EditTool } from "../../tool/plugin/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(AgentV2.ID.make(configuredDefault))
if (configuredDefault !== undefined) draft.default(Agent.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 = AgentV2.ID.make(id)
const agentID = Agent.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
@ -126,7 +126,7 @@ export const Plugin = define({
}),
})
function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset {
function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset {
// Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text:
// rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing.
return rules.map((rule) =>

View file

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

View file

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

View file

@ -1,10 +1,10 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { ProviderV2 } from "../../provider"
import { Provider } 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 = 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)
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)
})
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 = 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)
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)
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 = ProviderV2.mergeOverlay(existing.settings, variant.settings)
existing.settings = Provider.mergeOverlay(existing.settings, variant.settings)
if (variant.headers !== undefined)
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
}
}
if (config.cost !== undefined) {

View file

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

View file

@ -1,11 +1,11 @@
export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import path from "path"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill"
import { Skill } 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(
SkillV2.DirectorySource.make({
Skill.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(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
)
draft.source(
SkillV2.DirectorySource.make({
Skill.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(SkillV2.UrlSource.make({ type: "url", url: item }))
draft.source(Skill.UrlSource.make({ type: "url", url: item }))
continue
}
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source(
SkillV2.DirectorySource.make({
Skill.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
}),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { ProjectV2 } from "../project"
import { SessionV2 } from "../session"
import { Project } from "../project"
import { Session } 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: ProjectV2.ID,
actual: ProjectV2.ID,
expected: Project.ID,
actual: Project.ID,
},
) {}
@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSource
) {}
export type Error =
| SessionV2.NotFoundError
| Session.NotFoundError
| DestinationProjectMismatchError
| DestinationNotFoundError
| DestinationNotDirectoryError
| SessionV2.DestinationNotFoundError
| SessionV2.DestinationNotDirectoryError
| Session.DestinationNotFoundError
| Session.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* ProjectV2.Service
const project = yield* Project.Service
const sessions = yield* SessionStore.Service
const session = yield* SessionV2.Service
const session = yield* Session.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 SessionV2.NotFoundError({ sessionID: input.sessionID })
if (!current) return yield* new Session.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,
ProjectV2.node,
SessionV2.node,
Project.node,
Session.node,
SessionStore.node,
SessionExecution.node,
],

View file

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

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/v2/Credential") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/Credential") {}
const layer = Layer.effect(
Service,

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/v2/storage/Database") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/storage/Database") {}
const databaseLayer = Layer.effect(
Service,

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 { EventV2 } from "./event"
import { Bus } from "./bus"
const Types = new Set([
"agent.updated",
@ -13,12 +13,12 @@ const Types = new Set([
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const events = yield* EventV2.Service
const unsubscribe = yield* events.listen((event) =>
const bus = yield* Bus.Service
const unsubscribe = yield* bus.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: [EventV2.node] })
export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] })

View file

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

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/v2/FileMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/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 V2 integrations exist.
* Deferred until the corresponding integrations exist.
*/
// 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: 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: 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.

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/v2/FileSystem") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
const baseLayer = Layer.effect(
Service,

View file

@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
import os from "os"
import path from "path"
import { Config } from "../config"
import { EventV2 } from "../event"
import { Bus } from "../bus"
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 events = yield* EventV2.Service
const bus = yield* Bus.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 }) =>
events.publish(FileSystem.Event.Changed, {
bus.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, EventV2.node],
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node],
})

View file

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

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 { EventV2 } from "./event"
import { Bus } from "./bus"
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 const Event = Form.Event
export { Event } from "@opencode-ai/schema/form"
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 events = yield* EventV2.Service
const bus = yield* Bus.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* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
return form
}),
),
@ -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* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* 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* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* Cache.set(forms, id, { ...entry, state: next })
yield* 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: [EventV2.node] })
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
function validateAnswer(form: Info, answer: Answer) {
const fields = new Map(form.fields.map((field) => [field.key, field] as const))

View file

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

View file

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

View file

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

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/v2/InstructionDiscovery") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionDiscovery") {}
export const layer = (options?: Options) => Layer.effect(
Service,

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/v2/InstructionBuiltIns") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionBuiltIns") {}
const layer = Layer.effect(
Service,

View file

@ -20,7 +20,7 @@ import {
import { Integration } from "@opencode-ai/schema/integration"
import { Credential } from "./credential"
import { State } from "./state"
import { EventV2 } from "./event"
import { Bus } from "./bus"
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 const Event = Integration.Event
export { Event } from "@opencode-ai/schema/integration"
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/v2/Integration") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/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 events = yield* EventV2.Service
const bus = yield* Bus.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: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid),
})
const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => {
@ -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* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.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* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* bus.publish(Integration.Event.Updated, {})
}),
)
})
@ -706,24 +706,24 @@ const layer = Layer.effect(
label: input.label,
value: Credential.Key.make({ type: "key", key: input.key }),
})
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID })
yield* bus.publish(Integration.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* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.Updated, {})
}),
remove: Effect.fn("Integration.connection.remove")(function* (credentialID) {
const credential = yield* credentials.get(credentialID)
yield* credentials.remove(credentialID)
if (credential) {
yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID })
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID })
}
yield* events.publish(Event.Updated, {})
yield* bus.publish(Integration.Event.Updated, {})
}),
},
oauth: {
@ -809,5 +809,5 @@ const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Credential.node, EventV2.node, AppProcess.node],
deps: [Credential.node, Bus.node, AppProcess.node],
})

View file

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

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/v2/LocationMutation") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
interface ResolvedPath {
readonly canonical: string

View file

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

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 { EventV2 } from "../event"
import { Bus } from "../bus"
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/v2/MCP") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/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 events = yield* EventV2.Service
const bus = yield* Bus.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(events.publish(Command.Event.Updated, {})),
Effect.andThen(bus.publish(Command.Event.Updated, {})),
Effect.catch(() =>
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))),
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))),
),
)
@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect(
entry.tools = undefined
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
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)
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)
}),
),
)
@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect(
connection.onToolsChanged(() =>
live(
refreshTools(name, entry, connection).pipe(
Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })),
Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })),
),
),
)
connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection)))
connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name })))
connection.onResourcesChanged(() => live(bus.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* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
const scope = yield* Scope.fork(root)
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* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* 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)
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* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined)))
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* 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* 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)
})
// 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(
events.subscribe(Integration.Event.ConnectionUpdated).pipe(
bus.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* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.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* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.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* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
yield* bus.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, EventV2.node, Form.node, Integration.node, Credential.node],
deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
})
}

View file

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

View file

@ -15,17 +15,17 @@ import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { Credential } from "./credential"
import { Integration } from "./integration"
import { ModelV2 } from "./model"
import { Capabilities, ID, Info, Ref, VariantID } from "./model"
import { Npm } from "@opencode-ai/util/npm"
import { OpenAICodex } from "./plugin/provider/openai-codex"
import { ProviderV2 } from "./provider"
import { Provider } from "./provider"
export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnavailableError>()(
"SessionRunnerModel.VariantUnavailableError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: ModelV2.VariantID,
providerID: Provider.ID,
modelID: ID,
variant: 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: ProviderV2.ID,
modelID: ModelV2.ID,
providerID: Provider.ID,
modelID: 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: ModelV2.Ref
readonly ref: Ref
/** Catalog capabilities used to shape requests before provider lowering. */
readonly capabilities: ModelV2.Capabilities
readonly capabilities: Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
readonly cost: Info["cost"]
}
export interface Interface {
readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect<Resolved, Error>
readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error>
readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ModelResolver") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
const apiKey = (model: Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.settings?.apiKey
@ -74,7 +74,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
return undefined
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
const withDefaults = (model: 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: ModelV2.Info, route: AnyRoute) =>
limits: { context: model.limit.context, output: model.limit.output },
})
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const providerHeaders = (model: Info) => {
const packageName = Provider.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: ModelV2.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 ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
}
const providerOptions = (
model: ModelV2.Info,
model: Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (Object.keys(settings).length === 0) return undefined
const packageName = ProviderV2.packageName(model.package)
const packageName = Provider.packageName(model.package)
if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
@ -110,9 +110,9 @@ const providerOptions = (
}
export const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
model: Info,
variantID: VariantID | undefined,
): Effect.Effect<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 = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
draft.settings = Provider.mergeOverlay(draft.settings, variant.settings)
draft.headers = Provider.mergeHeaders(draft.headers, variant.headers)
draft.body = Provider.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = (
model: ModelV2.Info,
model: 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 = ProviderV2.mergeOverlay(draft.body, credential.metadata)
draft.body = Provider.mergeOverlay(draft.body, credential.metadata)
})
const packageName = ProviderV2.packageName(resolved.package)
const packageName = Provider.packageName(resolved.package)
const key = apiKey(resolved, credential)
if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) {
return Effect.succeed(codexModel(resolved, credential, key))
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
@ -164,7 +164,7 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
@ -172,7 +172,7 @@ export const fromCatalogModel = (
)
}
if (
ProviderV2.isAISDK(resolved.package) &&
Provider.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 (ProviderV2.isAISDK(resolved.package)) {
if (Provider.isAISDK(resolved.package)) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
draft.settings = Provider.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "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 ?? ProviderV2.loadPackage)(specifier).pipe(
const module = yield* (dependencies?.loadPackage ?? Provider.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: ModelV2.Info,
model: 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: ModelV2.Info) =>
const unsupported = (model: Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
@ -272,13 +272,13 @@ const unsupported = (model: ModelV2.Info) =>
})
export const resolveModel = (
model: ModelV2.Info,
variant: ModelV2.VariantID | undefined,
model: Info,
variant: VariantID | undefined,
credential?: Credential.Value,
dependencies?: Dependencies,
) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const supported = (model: ModelV2.Info) => Boolean(model.package)
export const supported = (model: Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */
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: ModelV2.Info,
variant?: ModelV2.VariantID,
selected: Info,
variant?: 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) => ProviderV2.loadPackage(specifier, npm),
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
return {
model,
ref: ModelV2.Ref.make({
ref: Ref.make({
id: selected.id,
providerID: selected.providerID,
...(variant === undefined ? {} : { variant }),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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