Compare commits

..

1 Commits

Author SHA1 Message Date
Test 44f4211357 feat(tui): route pasted question answers 2026-06-24 13:58:21 +00:00
291 changed files with 4086 additions and 4821 deletions
-1
View File
@@ -325,7 +325,6 @@ jobs:
run: bun run build run: bun run build
working-directory: packages/desktop working-directory: packages/desktop
env: env:
NODE_OPTIONS: --max-old-space-size=4096
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_ORG: ${{ vars.SENTRY_ORG }}
-2
View File
@@ -9,7 +9,6 @@ on:
- "bun.lock" - "bun.lock"
- "packages/storybook/**" - "packages/storybook/**"
- "packages/ui/**" - "packages/ui/**"
- "packages/session-ui/**"
pull_request: pull_request:
branches: [dev] branches: [dev]
paths: paths:
@@ -18,7 +17,6 @@ on:
- "bun.lock" - "bun.lock"
- "packages/storybook/**" - "packages/storybook/**"
- "packages/ui/**" - "packages/ui/**"
- "packages/session-ui/**"
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
+9
View File
@@ -57,6 +57,13 @@ The bounded projection of a Core-executed tool result persisted in Session histo
**Managed Tool Output File**: **Managed Tool Output File**:
A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history. A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history.
**Model Request Options**:
Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request.
_Avoid_: Request body, wire options
**Generation Controls**:
Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog.
**PTY Environment**: **PTY Environment**:
The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory. The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory.
@@ -107,6 +114,8 @@ The host-supplied environment overlay applied by the server when creating a PTY,
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix. - A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history. - Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn. - A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`. - The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter. - A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise. - A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
+28 -78
View File
@@ -29,12 +29,11 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@pierre/trees": "1.0.0-beta.4", "@pierre/trees": "1.0.0-beta.4",
"@sentry/solid": "catalog:", "@sentry/solid": "catalog:",
@@ -87,7 +86,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.17.10", "version": "1.17.9",
"bin": { "bin": {
"lildax": "./bin/lildax.cjs", "lildax": "./bin/lildax.cjs",
}, },
@@ -112,7 +111,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -148,7 +147,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -175,7 +174,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.82", "@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -197,7 +196,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -221,7 +220,7 @@
}, },
"packages/console/support": { "packages/console/support": {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-core": "workspace:*",
@@ -241,7 +240,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.17.10", "version": "1.17.9",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -334,7 +333,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"effect": "catalog:", "effect": "catalog:",
@@ -388,7 +387,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -402,7 +401,7 @@
}, },
"packages/effect-sqlite-node": { "packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
}, },
@@ -414,11 +413,10 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@hono/standard-validator": "catalog:", "@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
@@ -446,7 +444,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -462,7 +460,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83",
@@ -481,7 +479,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
@@ -500,7 +498,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.17.10", "version": "1.17.9",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -539,7 +537,6 @@
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*", "@opencode-ai/server": "workspace:*",
@@ -629,7 +626,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -679,7 +676,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -694,7 +691,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@opencode-ai/server", "name": "@opencode-ai/server",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
@@ -706,53 +703,9 @@
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
}, },
}, },
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.17.10",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@pierre/diffs": "catalog:",
"@shikijs/stream": "catalog:",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"diff": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"morphdom": "2.7.8",
"motion": "12.34.5",
"remeda": "catalog:",
"remend": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
"solid-list": "catalog:",
"strip-ansi": "7.1.2",
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
},
},
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -765,7 +718,7 @@
}, },
"packages/stats/app": { "packages/stats/app": {
"name": "@opencode-ai/stats-app", "name": "@opencode-ai/stats-app",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@opencode-ai/stats-core": "workspace:*", "@opencode-ai/stats-core": "workspace:*",
@@ -798,7 +751,7 @@
}, },
"packages/stats/core": { "packages/stats/core": {
"name": "@opencode-ai/stats-core", "name": "@opencode-ai/stats-core",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-athena": "3.933.0", "@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0", "@planetscale/database": "1.19.0",
@@ -817,7 +770,7 @@
}, },
"packages/stats/server": { "packages/stats/server": {
"name": "@opencode-ai/stats-server", "name": "@opencode-ai/stats-server",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-firehose": "3.933.0", "@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -836,7 +789,6 @@
"packages/storybook": { "packages/storybook": {
"name": "@opencode-ai/storybook", "name": "@opencode-ai/storybook",
"devDependencies": { "devDependencies": {
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@storybook/addon-a11y": "^10.2.13", "@storybook/addon-a11y": "^10.2.13",
@@ -858,7 +810,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@opencode-ai/tui", "name": "@opencode-ai/tui",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
@@ -885,9 +837,11 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@shikijs/stream": "catalog:", "@shikijs/stream": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
@@ -932,7 +886,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -1871,8 +1825,6 @@
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"], "@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"], "@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
@@ -5891,8 +5843,6 @@
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
+1 -2
View File
@@ -42,12 +42,11 @@ const inferenceEventTable = new aws.s3tables.Table(
{ name: "request", type: "string", required: false }, { name: "request", type: "string", required: false },
{ name: "client", type: "string", required: false }, { name: "client", type: "string", required: false },
{ name: "user_agent", type: "string", required: false }, { name: "user_agent", type: "string", required: false },
{ name: "model", type: "string", required: false },
{ name: "model_tier", type: "string", required: false },
{ name: "model_variant", type: "string", required: false }, { name: "model_variant", type: "string", required: false },
{ name: "source", type: "string", required: false }, { name: "source", type: "string", required: false },
{ name: "provider", type: "string", required: false }, { name: "provider", type: "string", required: false },
{ name: "provider_model", type: "string", required: false }, { name: "provider_model", type: "string", required: false },
{ name: "model", type: "string", required: false },
{ name: "llm_error_code", type: "int", required: false }, { name: "llm_error_code", type: "int", required: false },
{ name: "llm_error_message", type: "string", required: false }, { name: "llm_error_message", type: "string", required: false },
{ name: "error_response", type: "string", required: false }, { name: "error_response", type: "string", required: false },
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-gV5PVd9+MB5tM3pPH+S6YhGPRPXqHv5Ayw+8gngNlqY=", "x86_64-linux": "sha256-JJnJVqP2+NiiRVoTKXjGD09lPRyfz8YMfx0m3jBaUzU=",
"aarch64-linux": "sha256-1pxoseGyRwAawiPblo8FeaaAAdl0mkKRs6BS8qxxoh0=", "aarch64-linux": "sha256-tY3/IA+iZbOQQGxrcHMvQ1xqByskZyVI+4LZkU0wVjs=",
"aarch64-darwin": "sha256-81D/v2Ka7mtFHp4iM9eB9kjFcumSTvR07ABb1ahRLS0=", "aarch64-darwin": "sha256-9qMRrQKKOgQuOcAgAq8oZacVFD7G4nbAfZawmCRuJdY=",
"x86_64-darwin": "sha256-nolxEbc/1ItbmuHPUjaSu3DHwBV/0/Si2xuM6wpiq0k=" "x86_64-darwin": "sha256-dR2VGxDLoAPEmk8SsEF4dhlPBcb9ubztAuVfwxuAD4M="
} }
} }
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.17.10", "version": "1.17.9",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -47,7 +47,6 @@
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@pierre/trees": "1.0.0-beta.4", "@pierre/trees": "1.0.0-beta.4",
"@sentry/solid": "catalog:", "@sentry/solid": "catalog:",
+1 -1
View File
@@ -4,7 +4,7 @@ import { I18nProvider } from "@opencode-ai/ui/context"
import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { FileComponentProvider } from "@opencode-ai/ui/context/file" import { FileComponentProvider } from "@opencode-ai/ui/context/file"
import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { File } from "@opencode-ai/session-ui/file" import { File } from "@opencode-ai/ui/file"
import { Font } from "@opencode-ai/ui/font" import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo" import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { ThemeProvider } from "@opencode-ai/ui/theme/context"
@@ -7,8 +7,8 @@ import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Accordion } from "@opencode-ai/ui/accordion" import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { File } from "@opencode-ai/session-ui/file" import { File } from "@opencode-ai/ui/file"
import { Markdown } from "@opencode-ai/session-ui/markdown" import { Markdown } from "@opencode-ai/ui/markdown"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { QueryClient } from "@tanstack/solid-query" import { QueryClient } from "@tanstack/solid-query"
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap" import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { ServerScope } from "@/utils/server-scope" import { ServerScope } from "@/utils/server-scope"
@@ -19,7 +19,7 @@ import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query" import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery } from "../server-sync" import { loadMcpQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { ScopedKey, type ServerScope } from "@/utils/server-scope" import { ScopedKey, type ServerScope } from "@/utils/server-scope"
type GlobalStore = { type GlobalStore = {
@@ -1,7 +1,7 @@
import { beforeAll, describe, expect, mock, test } from "bun:test" import { beforeAll, describe, expect, mock, test } from "bun:test"
import { createRoot, getOwner, type Owner } from "solid-js" import { createRoot, getOwner, type Owner } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import type { State } from "./types" import type { State } from "./types"
import type { QueryOptionsApi } from "../server-sync" import type { QueryOptionsApi } from "../server-sync"
import { ServerScope } from "@/utils/server-scope" import { ServerScope } from "@/utils/server-scope"
@@ -17,7 +17,7 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQuery } from "@tanstack/solid-query" import { useQuery } from "@tanstack/solid-query"
import { QueryOptionsApi } from "../server-sync" import { QueryOptionsApi } from "../server-sync"
import { directoryKey, type DirectoryKey } from "./utils" import { directoryKey, type DirectoryKey } from "./utils"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import type { ServerScope } from "@/utils/server-scope" import type { ServerScope } from "@/utils/server-scope"
export function createChildStoreManager(input: { export function createChildStoreManager(input: {
@@ -15,7 +15,7 @@ import type {
Todo, Todo,
VcsInfo, VcsInfo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import type { Accessor } from "solid-js" import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store" import type { SetStoreFunction, Store } from "solid-js/store"
@@ -1,5 +1,5 @@
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
+1 -2
View File
@@ -29,8 +29,7 @@ import { createRefreshQueue } from "./global-sync/queue"
import { directoryKey } from "./global-sync/utils" import { directoryKey } from "./global-sync/utils"
import { PathKey } from "@/utils/path-key" import { PathKey } from "@/utils/path-key"
import { createDirSyncContext } from "./directory-sync" import { createDirSyncContext } from "./directory-sync"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import { createRefCountMap } from "@/utils/refcount" import { createRefCountMap } from "@/utils/refcount"
import { useGlobal } from "./global" import { useGlobal } from "./global"
import { ServerConnection, useServer } from "./server" import { ServerConnection, useServer } from "./server"
-1
View File
@@ -1,5 +1,4 @@
@import "@opencode-ai/ui/styles/tailwind"; @import "@opencode-ai/ui/styles/tailwind";
@import "@opencode-ai/session-ui/styles";
@import "@opencode-ai/ui/v2/styles/tailwind.css"; @import "@opencode-ai/ui/v2/styles/tailwind.css";
@font-face { @font-face {
+1 -1
View File
@@ -1,4 +1,4 @@
import { DataProvider } from "@opencode-ai/session-ui/context" import { DataProvider } from "@opencode-ai/ui/context"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLocation, useNavigate, useParams } from "@solidjs/router"
+1 -1
View File
@@ -60,7 +60,7 @@ import { ServerHealthIndicator } from "@/components/server/server-row"
import { type ServerHealth } from "@/utils/server-health" import { type ServerHealth } from "@/utils/server-health"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { useMarked } from "@opencode-ai/ui/context/marked" import { useMarked } from "@opencode-ai/ui/context/marked"
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" import { preloadMarkdown } from "@opencode-ai/ui/markdown-cache"
const HOME_SESSION_LIMIT = 64 const HOME_SESSION_LIMIT = 64
const HOME_ROW_LAYOUT = const HOME_ROW_LAYOUT =
@@ -3,7 +3,7 @@ import { getProjectAvatarVariant } from "@/context/layout"
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2" import { SessionProgressIndicatorV2 } from "@opencode-ai/ui/v2/session-progress-indicator-v2"
import { Show } from "solid-js" import { Show } from "solid-js"
export function SessionTabAvatar(props: { export function SessionTabAvatar(props: {
+1 -1
View File
@@ -25,7 +25,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Select } from "@opencode-ai/ui/select" import { Select } from "@opencode-ai/ui/select"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { createAutoScroll } from "@opencode-ai/ui/hooks" import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { base64Encode, checksum } from "@opencode-ai/core/util/encode" import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
@@ -1,7 +1,7 @@
import { For, Show } from "solid-js" import { For, Show } from "solid-js"
import type { PermissionRequest } from "@opencode-ai/sdk/v2" import type { PermissionRequest } from "@opencode-ai/sdk/v2"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt" import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -2,7 +2,7 @@ import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useMutation } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt" import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
+3 -3
View File
@@ -2,10 +2,10 @@ import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch }
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileSearchHandle } from "@opencode-ai/session-ui/file" import type { FileSearchHandle } from "@opencode-ai/ui/file"
import { useFileComponent } from "@opencode-ai/ui/context/file" import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations" import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations"
import { sampledChecksum } from "@opencode-ai/core/util/encode" import { sampledChecksum } from "@opencode-ai/core/util/encode"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -1,12 +1,12 @@
import { createEffect, onCleanup, type JSX } from "solid-js" import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/session-ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import type { import type {
SessionReviewCommentActions, SessionReviewCommentActions,
SessionReviewCommentDelete, SessionReviewCommentDelete,
SessionReviewCommentUpdate, SessionReviewCommentUpdate,
} from "@opencode-ai/session-ui/session-review" } from "@opencode-ai/ui/session-review"
import type { SelectedLineRange } from "@/context/file" import type { SelectedLineRange } from "@/context/file"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
@@ -26,7 +26,7 @@ import {
Part as MessagePart, Part as MessagePart,
partDefaultOpen, partDefaultOpen,
type UserActions, type UserActions,
} from "@opencode-ai/session-ui/message-part" } from "@opencode-ai/ui/message-part"
import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { FileIcon } from "@opencode-ai/ui/file-icon" import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
@@ -35,7 +35,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input" import { InlineInput } from "@opencode-ai/ui/inline-input"
import { Spinner } from "@opencode-ai/ui/spinner" import { Spinner } from "@opencode-ai/ui/spinner"
import { SessionRetry } from "@opencode-ai/session-ui/session-retry" import { SessionRetry } from "@opencode-ai/ui/session-retry"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
@@ -51,7 +51,7 @@ import type {
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover" import { Popover as KobaltePopover } from "@kobalte/core/popover"
import { normalize } from "@opencode-ai/session-ui/session-diff" import { normalize } from "@opencode-ai/ui/session-diff"
import { useFileComponent } from "@opencode-ai/ui/context/file" import { useFileComponent } from "@opencode-ai/ui/context/file"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage" import { SessionContextUsage } from "@/components/session-context-usage"
@@ -1,6 +1,6 @@
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
import { groupParts, PartGroup, renderable } from "@opencode-ai/session-ui/message-part" import { groupParts, PartGroup, renderable } from "@opencode-ai/ui/message-part"
import { Data, Equal } from "effect" import { Data, Equal } from "effect"
export type SummaryDiff = SnapshotFileDiff & { file: string } export type SummaryDiff = SnapshotFileDiff & { file: string }
@@ -1,7 +1,7 @@
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { useCommand, type CommandOption } from "@/context/command" import { useCommand, type CommandOption } from "@/context/command"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.17.10", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.17.10", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -196,7 +196,6 @@ export async function handler(
providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId) providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId)
Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => { Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => {
if (v === "$ip") return headers.set(k, ip) if (v === "$ip") return headers.set(k, ip)
if (v === "$caller") return headers.set(k, `caller:${ip}`)
if (v === "$session") return headers.set(k, sessionId) if (v === "$session") return headers.set(k, sessionId)
if (v === "$model") return headers.set(k, model) if (v === "$model") return headers.set(k, model)
if (v === "$request") return headers.set(k, requestId) if (v === "$request") return headers.set(k, requestId)
@@ -355,7 +354,7 @@ export async function handler(
responseLength += value.length responseLength += value.length
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
const parts = buffer.split(/\r\n\r\n|\n\n|\r\r/) const parts = buffer.split(providerInfo.streamSeparator)
buffer = parts.pop() ?? "" buffer = parts.pop() ?? ""
for (let part of parts) { for (let part of parts) {
@@ -139,6 +139,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
return encoder.encode(messages.join("")) return encoder.encode(messages.join(""))
} }
}, },
streamSeparator: "\n\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@@ -37,6 +37,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({
return body return body
}, },
createBinaryStreamDecoder: () => undefined, createBinaryStreamDecoder: () => undefined,
streamSeparator: "\r\n\r\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@@ -37,6 +37,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
} }
}, },
createBinaryStreamDecoder: () => undefined, createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@@ -20,6 +20,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
}, },
modifyBody: (body: Record<string, any>) => body, modifyBody: (body: Record<string, any>) => body,
createBinaryStreamDecoder: () => undefined, createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n",
createUsageParser: () => { createUsageParser: () => {
let usage: Usage let usage: Usage
@@ -44,6 +44,7 @@ export type ProviderHelper = (input: {
modifyHeaders: (headers: Headers, apiKey: string, stickyId: string) => void modifyHeaders: (headers: Headers, apiKey: string, stickyId: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any> modifyBody: (body: Record<string, any>) => Record<string, any>
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
streamSeparator: string
createUsageParser: () => { createUsageParser: () => {
parse: (chunk: string) => void parse: (chunk: string) => void
retrieve: () => any retrieve: () => any
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.17.10", "version": "1.17.9",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.17.10", "version": "1.17.9",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -118,12 +118,11 @@ function toLakeEvent(time: string, data: Record<string, unknown>) {
request: string(data, "request"), request: string(data, "request"),
client: string(data, "client"), client: string(data, "client"),
user_agent: string(data, "user_agent"), user_agent: string(data, "user_agent"),
model: string(data, "model"),
model_tier: string(data, "model.tier"),
model_variant: string(data, "model.variant"), model_variant: string(data, "model.variant"),
source: string(data, "source"), source: string(data, "source"),
provider: string(data, "provider"), provider: string(data, "provider"),
provider_model: string(data, "provider.model"), provider_model: string(data, "provider.model"),
model: string(data, "model"),
llm_error_code: integer(data, "llm.error.code"), llm_error_code: integer(data, "llm.error.code"),
llm_error_message: string(data, "llm.error.message"), llm_error_message: string(data, "llm.error.message"),
error_response: string(data, "error.response"), error_response: string(data, "error.response"),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.17.10", "version": "1.17.9",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.17.10", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+5 -4
View File
@@ -1,8 +1,8 @@
export * as Catalog from "./catalog" export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { ModelRequest } from "./model-request"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Policy } from "./policy" import { Policy } from "./policy"
@@ -18,7 +18,9 @@ export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export const PolicyActions = Schema.Literals(["provider.use"]) export const PolicyActions = Schema.Literals(["provider.use"])
export const Event = Catalog.Event export const Event = {
Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
}
type Data = { type Data = {
providers: Map<ProviderV2.ID, ProviderRecord> providers: Map<ProviderV2.ID, ProviderRecord>
@@ -84,8 +86,7 @@ export const layer = Layer.effect(
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api : model.api
const request = { const request = {
headers: { ...provider.request.headers, ...model.request.headers }, ...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request),
body: { ...provider.request.body, ...model.request.body },
variant: model.request.variant, variant: model.request.variant,
} }
return ModelV2.Info.make({ return ModelV2.Info.make({
+15 -4
View File
@@ -4,6 +4,7 @@ import { define } from "../../plugin/internal"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const Plugin = define({ export const Plugin = define({
@@ -58,11 +59,15 @@ export const Plugin = define({
Object.assign(provider.request.body, item.request.body) Object.assign(provider.request.body, item.request.body)
} }
}) })
const providerApi = catalog.provider.get(providerID)?.provider.api
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
for (const [id, config] of Object.entries(item.models ?? {})) { for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => { catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api } if (config.api !== undefined) model.api = { ...model.api, ...config.api }
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
if (config.capabilities !== undefined) { if (config.capabilities !== undefined) {
model.capabilities = { model.capabilities = {
tools: config.capabilities.tools, tools: config.capabilities.tools,
@@ -71,8 +76,10 @@ export const Plugin = define({
} }
} }
if (config.request !== undefined) { if (config.request !== undefined) {
Object.assign(model.request.headers, config.request.headers) ModelRequest.assign(model.request, {
Object.assign(model.request.body, config.request.body) headers: config.request.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
})
if (config.request.variant !== undefined) model.request.variant = config.request.variant if (config.request.variant !== undefined) model.request.variant = config.request.variant
} }
if (config.variants !== undefined) { if (config.variants !== undefined) {
@@ -83,11 +90,15 @@ export const Plugin = define({
id: variant.id, id: variant.id,
headers: {}, headers: {},
body: {}, body: {},
generation: {},
options: {},
} }
model.variants.push(existing) model.variants.push(existing)
} }
Object.assign(existing.headers, variant.headers) ModelRequest.assign(existing, {
Object.assign(existing.body, variant.body) headers: variant.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
})
} }
} }
if (config.cost !== undefined) { if (config.cost !== undefined) {
+94 -23
View File
@@ -1,19 +1,46 @@
export * as EventV2 from "./event" export * as EventV2 from "./event"
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Cause, Context, 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 { and, asc, eq, gt } from "drizzle-orm" import { and, asc, eq, gt } from "drizzle-orm"
import { Database } from "./database/database" import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql" import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location" import { Location } from "./location"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { LayerNode } from "./effect/layer-node" import { LayerNode } from "./effect/layer-node"
import { isDeepStrictEqual } from "node:util" import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Event.ID export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
export type ID = import("@opencode-ai/schema/event").ID Schema.brand("Event.ID"),
export type { Data, Definition, Payload } from "@opencode-ai/schema/event" withStatics((schema) => ({
create: () => schema.make("evt_" + Identifier.ascending()),
})),
)
export type ID = typeof ID.Type
export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly data: DataSchema
}
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
export type Payload<D extends Definition = Definition> = {
readonly id: ID
readonly type: D["type"]
readonly data: Data<D>
readonly durable?: {
readonly aggregateID: string
readonly seq: number
readonly version: number
}
readonly location?: Location.Ref
readonly metadata?: Record<string, unknown>
}
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void> export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void> export type Unsubscribe = Effect.Effect<void>
@@ -47,8 +74,52 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
}, },
) {} ) {}
export const define = Event.define export function versionedType(type: string, version: number) {
export const versionedType = Event.versionedType return `${type}.${version}`
}
export const registry = new Map<string, Definition>()
const durableRegistry = new Map<string, Definition>()
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly schema: Fields
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
const Data = Schema.Struct(input.schema)
const Payload = Schema.Struct({
id: ID,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
type: Schema.Literal(input.type),
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
location: Schema.optional(Location.Ref),
data: Data,
}).annotate({ identifier: input.type })
const definition = Object.assign(Payload, {
type: input.type,
...(input.durable === undefined ? {} : { durable: input.durable }),
data: Data,
})
const existing = registry.get(input.type)
if (
input.durable === undefined ||
existing?.durable === undefined ||
input.durable.version >= existing.durable.version
) {
registry.set(input.type, definition)
}
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
Definition<Type, Schema.Struct<Fields>>
}
export function definitions() {
return registry.values().toArray()
}
export interface PublishOptions { export interface PublishOptions {
readonly id?: ID readonly id?: ID
@@ -98,7 +169,6 @@ export const layerWith = (options?: LayerOptions) =>
typed: new Map<string, PubSub.PubSub<Payload>>(), typed: new Map<string, PubSub.PubSub<Payload>>(),
} }
const projectors = new Map<string, Subscriber[]>() const projectors = new Map<string, Subscriber[]>()
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
const listeners = new Array<Subscriber>() const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service const { db } = yield* Database.Service
@@ -124,7 +194,6 @@ export const layerWith = (options?: LayerOptions) =>
) )
function commitDurableEvent( function commitDurableEvent(
definition: Definition,
event: Payload, event: Payload,
input?: { input?: {
readonly seq: number readonly seq: number
@@ -135,6 +204,7 @@ export const layerWith = (options?: LayerOptions) =>
commit?: (seq: number) => Effect.Effect<void>, commit?: (seq: number) => Effect.Effect<void>,
) { ) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = registry.get(event.type)
const durable = definition?.durable const durable = definition?.durable
if (durable) { if (durable) {
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate] const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
@@ -168,10 +238,9 @@ export const layerWith = (options?: LayerOptions) =>
.get() .get()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const latest = row?.seq ?? -1 const latest = row?.seq ?? -1
const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record< const encoded = Schema.encodeUnknownSync(
string, definition.data as Schema.Codec<unknown, unknown, never, never>,
unknown )(event.data) as Record<string, unknown>
>
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) { if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
yield* Effect.die( yield* Effect.die(
new InvalidDurableEventError({ new InvalidDurableEventError({
@@ -287,8 +356,9 @@ export const layerWith = (options?: LayerOptions) =>
}) })
} }
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) { function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = registry.get(event.type)
if (!definition?.durable && commit) if (!definition?.durable && commit)
return yield* Effect.die( return yield* Effect.die(
new InvalidDurableEventError({ new InvalidDurableEventError({
@@ -297,7 +367,7 @@ export const layerWith = (options?: LayerOptions) =>
}), }),
) )
if (definition?.durable) { if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) const committed = yield* commitDurableEvent(event as Payload, undefined, commit)
if (committed) { if (committed) {
event = { event = {
...event, ...event,
@@ -346,7 +416,6 @@ export const layerWith = (options?: LayerOptions) =>
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined) : undefined)
return yield* publishEvent( return yield* publishEvent(
definition,
{ {
id: options?.id ?? ID.create(), id: options?.id ?? ID.create(),
...(options?.metadata ? { metadata: options.metadata } : {}), ...(options?.metadata ? { metadata: options.metadata } : {}),
@@ -364,7 +433,7 @@ export const layerWith = (options?: LayerOptions) =>
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
) { ) {
return Effect.gen(function* () { return Effect.gen(function* () {
const definition = Durable.get(event.type) const definition = durableRegistry.get(event.type)
if (!definition?.durable) { if (!definition?.durable) {
yield* Effect.die( yield* Effect.die(
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
@@ -373,9 +442,11 @@ export const layerWith = (options?: LayerOptions) =>
const payload = { const payload = {
id: event.id, id: event.id,
type: definition.type, type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data), data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(
event.data,
),
} as Payload } as Payload
const committed = yield* commitDurableEvent(definition, payload, { const committed = yield* commitDurableEvent(payload, {
seq: event.seq, seq: event.seq,
aggregateID: event.aggregateID, aggregateID: event.aggregateID,
ownerID: options?.ownerID, ownerID: options?.ownerID,
@@ -459,8 +530,8 @@ export const layerWith = (options?: LayerOptions) =>
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all) const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
const decodeSerializedEvent = (event: SerializedEvent) => { const decodeSerializedEvent = (event: SerializedEvent): Payload => {
const definition = Durable.get(event.type) const definition = durableRegistry.get(event.type)
if (!definition?.durable) { if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
} }
@@ -468,7 +539,7 @@ export const layerWith = (options?: LayerOptions) =>
id: event.id, id: event.id,
type: definition.type, type: definition.type,
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version }, durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
data: Schema.decodeUnknownSync(definition.data)(event.data), data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data),
} }
} }
+10 -2
View File
@@ -2,11 +2,12 @@ export * as FileSystem from "./filesystem"
import path from "path" import path from "path"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { EventV2 } from "./event"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
import { Location } from "./location" import { Location } from "./location"
import { PositiveInt, RelativePath } from "./schema" import { PositiveInt, RelativePath } from "./schema"
import { FileSystemSearch } from "./filesystem/search" import { FileSystemSearch } from "./filesystem/search"
import { Entry, FileSystem, Match } from "@opencode-ai/schema/filesystem" import { Entry, Match } from "@opencode-ai/schema/filesystem"
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
export const ReadInput = Schema.Struct({ export const ReadInput = Schema.Struct({
@@ -47,7 +48,14 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
limit: PositiveInt.pipe(Schema.optional), limit: PositiveInt.pipe(Schema.optional),
}) {} }) {}
export const Event = FileSystem.Event export const Event = {
Edited: EventV2.define({
type: "file.edited",
schema: {
file: Schema.String,
},
}),
}
export interface Interface { export interface Interface {
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
+10 -3
View File
@@ -3,8 +3,7 @@ export * as Watcher from "./watcher"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher" import type ParcelWatcher from "@parcel/watcher"
import { Cause, Context, Effect, Layer } from "effect" import { Cause, Context, Effect, Layer, Schema } from "effect"
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
import path from "path" import path from "path"
import { Config } from "../config" import { Config } from "../config"
import { EventV2 } from "../event" import { EventV2 } from "../event"
@@ -20,7 +19,15 @@ declare const OPENCODE_LIBC: string | undefined
const SUBSCRIBE_TIMEOUT_MS = 10_000 const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = FileSystemWatcher.Event export const Event = {
Updated: EventV2.define({
type: "file.watcher.updated",
schema: {
file: Schema.String,
event: Schema.Literals(["add", "change", "unlink"]),
},
}),
}
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try { try {
+10 -1
View File
@@ -136,7 +136,16 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError export type Error = CodeRequiredError | AuthorizationError
export const Event = Integration.Event export const Event = {
Updated: EventV2.define({
type: "integration.updated",
schema: {},
}),
ConnectionUpdated: EventV2.define({
type: "integration.connection.updated",
schema: { integrationID: ID },
}),
}
export const Ref = Integration.Ref export const Ref = Integration.Ref
export type Ref = Integration.Ref export type Ref = Integration.Ref
+102
View File
@@ -0,0 +1,102 @@
export * as ModelRequest from "./model-request"
import { ModelRequest } from "@opencode-ai/schema/model-request"
export const Generation = ModelRequest.Generation
export type Generation = ModelRequest.Generation
export const Request = ModelRequest.Request
export type Request = ModelRequest.Request
interface MutableRequest {
headers: Record<string, string>
body: Record<string, unknown>
generation?: Record<string, unknown>
options?: Record<string, unknown>
}
const generationKeys = new Map<string, keyof Generation>([
["maxOutputTokens", "maxTokens"],
["maxTokens", "maxTokens"],
["temperature", "temperature"],
["topP", "topP"],
["topK", "topK"],
["frequencyPenalty", "frequencyPenalty"],
["presencePenalty", "presencePenalty"],
["seed", "seed"],
["stopSequences", "stop"],
["stop", "stop"],
])
interface Profile {
readonly namespace: string
readonly semantics: ReadonlyMap<string, string>
}
const profiles = new Map<string, Profile>([
[
"@ai-sdk/openai",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoningSummary", "reasoningSummary"],
["include", "include"],
["textVerbosity", "textVerbosity"],
["serviceTier", "serviceTier"],
["service_tier", "serviceTier"],
]),
},
],
[
"@ai-sdk/openai-compatible",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoning_effort", "reasoningEffort"],
]),
},
],
["@ai-sdk/anthropic", { namespace: "anthropic", semantics: new Map([["thinking", "thinking"]]) }],
])
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
export const merge = (base: Request, override: Partial<Request>) => ({
headers: { ...base.headers, ...override.headers },
body: { ...base.body, ...override.body },
generation: { ...base.generation, ...override.generation },
options: { ...base.options, ...override.options },
})
export const assign = (target: MutableRequest, override: Partial<Request>) => {
Object.assign(target.headers, override.headers)
Object.assign(target.body, override.body)
Object.assign((target.generation ??= {}), override.generation)
Object.assign((target.options ??= {}), override.options)
}
/** Partitions AI-SDK-shaped request options before they enter the Catalog. */
export function normalizeAiSdkOptions(packageName: string | undefined, input: Readonly<Record<string, unknown>>) {
const generation: Record<string, number | ReadonlyArray<string>> = {}
const options: Record<string, unknown> = {}
const body: Record<string, unknown> = {}
const semantics = profiles.get(packageName ?? "")?.semantics
for (const [key, value] of Object.entries(input)) {
const generationKey = generationKeys.get(key)
if (generationKey === "stop" && Array.isArray(value) && value.every((item) => typeof item === "string"))
generation[generationKey] = value
else if (generationKey !== undefined && generationKey !== "stop" && typeof value === "number")
generation[generationKey] = value
else if (semantics?.has(key)) options[semantics.get(key)!] = value
else body[key] = value
}
return { generation, options, body }
}
+6 -2
View File
@@ -1,7 +1,6 @@
import path from "path" import path from "path"
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Global } from "./global" import { Global } from "./global"
import { Flag } from "./flag/flag" import { Flag } from "./flag/flag"
import { Flock } from "./util/flock" import { Flock } from "./util/flock"
@@ -109,7 +108,12 @@ export const Provider = Schema.Struct({
export type Provider = Schema.Schema.Type<typeof Provider> export type Provider = Schema.Schema.Type<typeof Provider>
export const Event = ModelsDev.Event export const Event = {
Refreshed: EventV2.define({
type: "models-dev.refreshed",
schema: {},
}),
}
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
+35 -11
View File
@@ -7,31 +7,45 @@ import { Location } from "./location"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { SessionV2 } from "./session" import { SessionV2 } from "./session"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { Wildcard } from "./util/wildcard" import { Wildcard } from "./util/wildcard"
import { PermissionSaved } from "./permission/saved" import { PermissionSaved } from "./permission/saved"
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Permission.ID export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionV2.ID"),
withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
)
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Source = Permission.Source export const Source = Schema.Union([
Schema.Struct({
type: Schema.Literal("tool"),
messageID: Schema.String,
callID: Schema.String,
}),
]).annotate({ identifier: "PermissionV2.Source" })
export type Source = typeof Source.Type export type Source = typeof Source.Type
const RequestFields = { const RequestFields = {
sessionID: Permission.Request.fields.sessionID, sessionID: SessionV2.ID,
action: Permission.Request.fields.action, action: Schema.String,
resources: Permission.Request.fields.resources, resources: Schema.Array(Schema.String),
save: Permission.Request.fields.save, save: Schema.Array(Schema.String).pipe(Schema.optional),
metadata: Permission.Request.fields.metadata, metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
source: Permission.Request.fields.source, source: Source.pipe(Schema.optional),
} }
export const Request = Permission.Request export const Request = Schema.Struct({
id: ID,
...RequestFields,
}).annotate({ identifier: "PermissionV2.Request" })
export type Request = typeof Request.Type export type Request = typeof Request.Type
export const Reply = Permission.Reply export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
export type Reply = typeof Reply.Type export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({ export const AssertInput = Schema.Struct({
@@ -54,7 +68,17 @@ export const AskResult = Schema.Struct({
}).annotate({ identifier: "PermissionV2.AskResult" }) }).annotate({ identifier: "PermissionV2.AskResult" })
export type AskResult = typeof AskResult.Type export type AskResult = typeof AskResult.Type
export const Event = Permission.Event export const Event = {
Asked: EventV2.define({ type: "permission.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "permission.v2.replied",
schema: {
sessionID: SessionV2.ID,
requestID: ID,
reply: Reply,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {} export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
+10 -4
View File
@@ -1,8 +1,7 @@
export * as PluginV2 from "./plugin" export * as PluginV2 from "./plugin"
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect" import { Context, Deferred, Effect, Exit, Layer, Schema, Scope } from "effect"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { PluginEvent, PluginID } from "@opencode-ai/schema/plugin"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk" import { AISDK } from "./aisdk"
import { Catalog } from "./catalog" import { Catalog } from "./catalog"
@@ -15,10 +14,17 @@ import { Reference } from "./reference"
import { SkillV2 } from "./skill" import { SkillV2 } from "./skill"
import { State } from "./state" import { State } from "./state"
export const ID = PluginID export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Event = PluginEvent export const Event = {
Added: EventV2.define({
type: "plugin.added",
schema: {
id: ID,
},
}),
}
export interface Interface { export interface Interface {
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void> readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
+1 -3
View File
@@ -29,7 +29,6 @@ import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev" import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider" import { ProviderPlugins } from "./provider"
import { SkillPlugin } from "./skill" import { SkillPlugin } from "./skill"
import { VariantPlugin } from "./variant"
export type Requirements = export type Requirements =
| AgentV2.Service | AgentV2.Service
@@ -108,13 +107,12 @@ export const locationLayer = Layer.effectDiscard(
yield* add(CommandPlugin.Plugin) yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin) yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin) yield* add(ModelsDevPlugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item) for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin) yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true })) }).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}), }),
).pipe( ).pipe(
+11 -7
View File
@@ -2,6 +2,7 @@ import { define } from "./internal"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider" import { ProviderV2 } from "../provider"
@@ -37,12 +38,15 @@ function cost(input: ModelsDev.Model["cost"]) {
] ]
} }
function variants(model: ModelsDev.Model) { function variants(model: ModelsDev.Model, packageName?: string) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({ return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => {
id: ModelV2.VariantID.make(id), const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
headers: { ...(item.provider?.headers ?? {}) }, return {
body: { ...(item.provider?.body ?? {}) }, id: ModelV2.VariantID.make(id),
})) headers: { ...(item.provider?.headers ?? {}) },
...request,
}
})
} }
export const ModelsDevPlugin = define({ export const ModelsDevPlugin = define({
@@ -111,7 +115,7 @@ export const ModelsDevPlugin = define({
input: [...(model.modalities?.input ?? [])], input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])], output: [...(model.modalities?.output ?? [])],
} }
draft.variants = variants(model) draft.variants = variants(model, model.provider?.npm ?? item.npm)
draft.time.released = released(model.release_date) draft.time.released = released(model.release_date)
draft.cost = cost(model.cost) draft.cost = cost(model.cost)
draft.status = model.status ?? "active" draft.status = model.status ?? "active"
@@ -8,9 +8,9 @@ import { EventV2 } from "../../event"
import { Credential } from "../../credential" import { Credential } from "../../credential"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider" import { ConfigProviderV1 } from "../../v1/config/provider"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
import { ConfigV1 } from "../../v1/config/config" import { ConfigV1 } from "../../v1/config/config"
const defaultServer = "https://console.opencode.ai" const defaultServer = "https://console.opencode.ai"
@@ -142,14 +142,15 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input] if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output] if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm const packageName = config.provider?.npm ?? item.npm
const lowerer = ConfigProviderOptionsV1.get(packageName) ModelRequest.assign(model.request, {
Object.assign(model.request.headers, config.headers) headers: config.headers,
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options))) ...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(config.options)),
})
if (config.variants !== undefined) { if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({ model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id), id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) }, headers: { ...(options.headers ?? {}) },
body: lowerer.request(withoutCredentials(options)), ...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(options)),
})) }))
} }
if (config.release_date !== undefined) { if (config.release_date !== undefined) {
-39
View File
@@ -1,39 +0,0 @@
export * as VariantPlugin from "./variant"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect } from "effect"
import { define } from "./internal"
export const Plugin = define({
id: "variant",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((catalog) => {
for (const record of catalog.provider.list()) {
for (const model of record.models.values()) {
catalog.model.update(model.providerID, model.id, (draft) => {
const generated = generate(draft)
if (generated.length === 0) return
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set(generated.map((variant) => variant.id))
draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant),
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
]
})
}
}
})
}),
})
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.api.id}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { reasoning_effort: id },
}))
}
+6 -2
View File
@@ -13,7 +13,6 @@ import { Slug } from "../util/slug"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { Database } from "../database/database" import { Database } from "../database/database"
import { Location } from "../location" import { Location } from "../location"
import { ProjectDirectoriesEvent } from "@opencode-ai/schema/project-directories"
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
export type StrategyID = typeof StrategyID.Type export type StrategyID = typeof StrategyID.Type
@@ -107,7 +106,12 @@ export interface Strategy {
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError> readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
} }
export const Event = ProjectDirectoriesEvent export const Event = {
Updated: EventV2.define({
type: "project.directories.updated",
schema: { projectID: Project.ID },
}),
}
export interface Interface { export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError> readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
+19 -4
View File
@@ -2,11 +2,10 @@ export * as Pty from "./pty"
import type { Disp, Proc } from "#pty" import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { PtyEvent, PtyInfo } from "@opencode-ai/schema/pty"
import { Config } from "./config" import { Config } from "./config"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Location } from "./location" import { Location } from "./location"
import { PositiveInt } from "./schema" import { NonNegativeInt, PositiveInt } from "./schema"
import { PtyID } from "./pty/schema" import { PtyID } from "./pty/schema"
import { Shell } from "./shell" import { Shell } from "./shell"
import { lazy } from "./util/lazy" import { lazy } from "./util/lazy"
@@ -36,7 +35,18 @@ type Active = {
listeners: Disp[] listeners: Disp[]
} }
export const Info = PtyInfo export const Info = Schema.Struct({
id: PtyID,
title: Schema.String,
command: Schema.String,
args: Schema.Array(Schema.String),
cwd: Schema.String,
status: Schema.Literals(["running", "exited"]),
// Windows ConPTY assigns the child pid asynchronously, so 0 is valid at spawn time.
pid: NonNegativeInt,
// Present once status is "exited".
exitCode: Schema.optional(NonNegativeInt),
}).annotate({ identifier: "Pty" })
export type Info = Types.DeepMutable<typeof Info.Type> export type Info = Types.DeepMutable<typeof Info.Type>
@@ -90,7 +100,12 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
ptyID: PtyID, ptyID: PtyID,
}) {} }) {}
export const Event = PtyEvent export const Event = {
Created: EventV2.define({ type: "pty.created", schema: { info: Info } }),
Updated: EventV2.define({ type: "pty.updated", schema: { info: Info } }),
Exited: EventV2.define({ type: "pty.exited", schema: { id: PtyID, exitCode: NonNegativeInt } }),
Deleted: EventV2.define({ type: "pty.deleted", schema: { id: PtyID } }),
}
export interface Interface { export interface Interface {
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
+13 -1
View File
@@ -1 +1,13 @@
export { ID as PtyID } from "@opencode-ai/schema/pty" import { Schema } from "effect"
import { Identifier } from "../id/id"
import { withStatics } from "../schema"
const ptyIdSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
export type PtyID = typeof ptyIdSchema.Type
export const PtyID = ptyIdSchema.pipe(
withStatics((schema: typeof ptyIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
})),
)
@@ -1,3 +0,0 @@
export * as PublicEventManifest from "./public-event-manifest"
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
+58 -10
View File
@@ -1,35 +1,83 @@
export * as QuestionV2 from "./question" export * as QuestionV2 from "./question"
import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Identifier } from "./id/id"
import { withStatics } from "./schema"
import { SessionSchema } from "./session/schema" import { SessionSchema } from "./session/schema"
export const ID = Question.ID export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
Schema.brand("QuestionV2.ID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(Identifier.ascending("question", id)) })),
)
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Option = Question.Option export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionV2.Option" })
export type Option = typeof Option.Type export type Option = typeof Option.Type
export const Info = Question.Info const base = {
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }),
}
export const Info = Schema.Struct({
...base,
custom: Schema.Boolean.pipe(Schema.optional).annotate({
description: "Allow typing a custom answer (default: true)",
}),
}).annotate({ identifier: "QuestionV2.Info" })
export type Info = typeof Info.Type export type Info = typeof Info.Type
export const Prompt = Question.Prompt export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
export type Prompt = typeof Prompt.Type export type Prompt = typeof Prompt.Type
export const Tool = Question.Tool export const Tool = Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).annotate({ identifier: "QuestionV2.Tool" })
export type Tool = typeof Tool.Type export type Tool = typeof Tool.Type
export const Request = Question.Request export const Request = Schema.Struct({
id: ID,
sessionID: SessionSchema.ID,
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
tool: Tool.pipe(Schema.optional),
}).annotate({ identifier: "QuestionV2.Request" })
export type Request = typeof Request.Type export type Request = typeof Request.Type
export const Answer = Question.Answer export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
export type Answer = typeof Answer.Type export type Answer = typeof Answer.Type
export const Reply = Question.Reply export const Reply = Schema.Struct({
answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)",
}),
}).annotate({ identifier: "QuestionV2.Reply" })
export type Reply = typeof Reply.Type export type Reply = typeof Reply.Type
export const Event = Question.Event export const Event = {
Asked: EventV2.define({ type: "question.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "question.v2.replied",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
answers: Schema.Array(Answer),
},
}),
Rejected: EventV2.define({
type: "question.v2.rejected",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) {
override get message() { override get message() {
+3 -1
View File
@@ -18,7 +18,9 @@ export type GitSource = Reference.GitSource
export const Source = Reference.Source export const Source = Reference.Source
export type Source = Reference.Source export type Source = Reference.Source
export const Event = Reference.Event export const Event = {
Updated: EventV2.define({ type: "reference.updated", schema: {} }),
}
export class Info extends Schema.Class<Info>("Reference.Info")({ export class Info extends Schema.Class<Info>("Reference.Info")({
name: Schema.String, name: Schema.String,
+468 -2
View File
@@ -1,2 +1,468 @@
export * from "@opencode-ai/schema/session-event" import { Schema } from "effect"
export * as SessionEvent from "@opencode-ai/schema/session-event" import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm"
import { Delivery } from "@opencode-ai/schema/session-delivery"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "../schema"
import { FileAttachment, Prompt } from "./prompt"
import { SessionSchema } from "./schema"
import { Location } from "../location"
import { SessionMessageID } from "./message-id"
import { SessionMessage } from "./message"
export { FileAttachment }
export const Source = Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
text: Schema.String,
}).annotate({
identifier: "session.next.event.source",
})
export type Source = typeof Source.Type
const Base = {
timestamp: DateTimeUtcFromMillis,
sessionID: SessionSchema.ID,
}
const PromptFields = {
...Base,
messageID: SessionMessageID.ID,
prompt: Prompt,
delivery: Delivery,
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const stepSettlementOptions = {
durable: {
aggregate: "sessionID",
version: 2,
},
} as const
export const UnknownError = SessionMessage.UnknownError
export type UnknownError = SessionMessage.UnknownError
export const AgentSwitched = EventV2.define({
type: "session.next.agent.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
agent: Schema.String,
},
})
export type AgentSwitched = typeof AgentSwitched.Type
export const ModelSwitched = EventV2.define({
type: "session.next.model.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
model: ModelV2.Ref,
},
})
export type ModelSwitched = typeof ModelSwitched.Type
export const Moved = EventV2.define({
type: "session.next.moved",
...options,
schema: {
...Base,
location: Location.Ref,
subdirectory: RelativePath.pipe(Schema.optional),
},
})
export type Moved = typeof Moved.Type
export const Prompted = EventV2.define({
type: "session.next.prompted",
...options,
schema: PromptFields,
})
export type Prompted = typeof Prompted.Type
export const PromptAdmitted = EventV2.define({
type: "session.next.prompt.admitted",
...options,
schema: PromptFields,
})
export type PromptAdmitted = typeof PromptAdmitted.Type
export const ContextUpdated = EventV2.define({
type: "session.next.context.updated",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = EventV2.define({
type: "session.next.synthetic",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Synthetic = typeof Synthetic.Type
export namespace Shell {
export const Started = EventV2.define({
type: "session.next.shell.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
callID: Schema.String,
command: Schema.String,
},
})
export type Started = typeof Started.Type
export const Ended = EventV2.define({
type: "session.next.shell.ended",
...options,
schema: {
...Base,
callID: Schema.String,
output: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Step {
export const Started = EventV2.define({
type: "session.next.step.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
agent: Schema.String,
model: ModelV2.Ref,
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
export const Ended = EventV2.define({
type: "session.next.step.ended",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
finish: Schema.String,
cost: Schema.Finite,
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
export const Failed = EventV2.define({
type: "session.next.step.failed",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
error: UnknownError,
},
})
export type Failed = typeof Failed.Type
}
export namespace Text {
export const Started = EventV2.define({
type: "session.next.text.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
export const Delta = EventV2.define({
type: "session.next.text.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.text.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Reasoning {
export const Started = EventV2.define({
type: "session.next.reasoning.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
export const Delta = EventV2.define({
type: "session.next.reasoning.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.reasoning.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
text: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
}
export namespace Tool {
const ToolBase = {
...Base,
assistantMessageID: SessionMessageID.ID,
callID: Schema.String,
}
export namespace Input {
export const Started = EventV2.define({
type: "session.next.tool.input.started",
...options,
schema: {
...ToolBase,
name: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
export const Delta = EventV2.define({
type: "session.next.tool.input.delta",
schema: {
...ToolBase,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.tool.input.ended",
...options,
schema: {
...ToolBase,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export const Called = EventV2.define({
type: "session.next.tool.called",
...options,
schema: {
...ToolBase,
tool: Schema.String,
input: Schema.Record(Schema.String, Schema.Unknown),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Called = typeof Called.Type
/**
* Replayable bounded running-tool state. Tools should checkpoint semantic
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
*/
export const Progress = EventV2.define({
type: "session.next.tool.progress",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
},
})
export type Progress = typeof Progress.Type
export const Success = EventV2.define({
type: "session.next.tool.success",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Success = typeof Success.Type
export const Failed = EventV2.define({
type: "session.next.tool.failed",
...options,
schema: {
...ToolBase,
error: UnknownError,
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Failed = typeof Failed.Type
}
export const RetryError = Schema.Struct({
message: Schema.String,
statusCode: Schema.Finite.pipe(Schema.optional),
isRetryable: Schema.Boolean,
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
responseBody: Schema.String.pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
}).annotate({
identifier: "session.next.retry_error",
})
export type RetryError = typeof RetryError.Type
export const Retried = EventV2.define({
type: "session.next.retried",
...options,
schema: {
...Base,
attempt: Schema.Finite,
error: RetryError,
},
})
export type Retried = typeof Retried.Type
export namespace Compaction {
export const Started = EventV2.define({
type: "session.next.compaction.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
},
})
export type Started = typeof Started.Type
export const Delta = EventV2.define({
type: "session.next.compaction.delta",
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.compaction.ended",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Started.data.fields.reason,
text: Schema.String,
recent: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
const DurableDefinitions = [
AgentSwitched,
ModelSwitched,
Moved,
Prompted,
PromptAdmitted,
ContextUpdated,
Synthetic,
Shell.Started,
Shell.Ended,
Step.Started,
Step.Ended,
Step.Failed,
Text.Started,
Text.Ended,
Tool.Input.Started,
Tool.Input.Ended,
Tool.Called,
Tool.Progress,
Tool.Success,
Tool.Failed,
Reasoning.Started,
Reasoning.Ended,
Retried,
Compaction.Started,
Compaction.Ended,
] as const
const EphemeralDefinitions = [Text.Delta, Tool.Input.Delta, Reasoning.Delta, Compaction.Delta] as const
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union([...DurableDefinitions, ...EphemeralDefinitions], { mode: "oneOf" }).pipe(
Schema.toTaggedUnion("type"),
)
export type Event = typeof All.Type
export type Type = Event["type"]
export * as SessionEvent from "./event"
+6 -2
View File
@@ -11,6 +11,7 @@ import { Catalog } from "../../catalog"
import { Credential } from "../../credential" import { Credential } from "../../credential"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema" import { SessionSchema } from "../schema"
@@ -87,6 +88,8 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
} }
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
const options = model.request.options ?? {}
const namespace = model.api.type === "aisdk" ? ModelRequest.namespace(model.api.package) : undefined
const body = model.request.body const body = model.request.body
const httpBody = Object.hasOwn(body, "apiKey") const httpBody = Object.hasOwn(body, "apiKey")
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey")) ? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
@@ -95,6 +98,8 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
provider: model.providerID, provider: model.providerID,
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url }, endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
headers: model.request.headers, headers: model.request.headers,
generation: model.request.generation,
providerOptions: namespace && Object.keys(options).length > 0 ? { [namespace]: options } : undefined,
http: { body: httpBody }, http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output }, limits: { context: model.limit.context, output: model.limit.output },
}) })
@@ -117,8 +122,7 @@ const withVariant = (
return Effect.succeed( return Effect.succeed(
variant variant
? produce(model, (draft) => { ? produce(model, (draft) => {
Object.assign(draft.request.headers, variant.headers) ModelRequest.assign(draft.request, variant)
Object.assign(draft.request.body, variant.body)
}) })
: model, : model,
) )
+17 -4
View File
@@ -1,17 +1,30 @@
export * as SessionTodo from "./todo" export * as SessionTodo from "./todo"
import { asc, eq } from "drizzle-orm" import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { SessionTodo, SessionTodoInfo } from "@opencode-ai/schema/session-todo"
import { Database } from "../database/database" import { Database } from "../database/database"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { TodoTable } from "./sql" import { TodoTable } from "./sql"
export const Info = SessionTodoInfo export const Info = Schema.Struct({
content: Schema.String.annotate({ description: "Brief description of the task" }),
status: Schema.String.annotate({
description: "Current status of the task: pending, in_progress, completed, cancelled",
}),
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
}).annotate({ identifier: "SessionTodo.Info" })
export type Info = typeof Info.Type export type Info = typeof Info.Type
export const Event = SessionTodo.Event export const Event = {
Updated: EventV2.define({
type: "todo.updated",
schema: {
sessionID: SessionSchema.ID,
todos: Schema.Array(Info),
},
}),
}
export interface Interface { export interface Interface {
readonly update: (input: { readonly update: (input: {
+7 -2
View File
@@ -6,6 +6,7 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission" import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider" import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options" import { ConfigProviderOptionsV1 } from "./provider-options"
import { ModelRequest } from "../../model-request"
const keys = new Set([ const keys = new Set([
"logLevel", "logLevel",
@@ -191,7 +192,11 @@ function migrateProvider(info: ConfigProviderV1.Info) {
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) { function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
const packageID = info.provider?.npm ?? packageName const packageID = info.provider?.npm ?? packageName
const lowerer = ConfigProviderOptionsV1.get(packageID) const lowerer = ConfigProviderOptionsV1.get(packageID)
const request = info.options && lowerer.request(info.options) const ingest = (options: Readonly<Record<string, unknown>>) => {
const request = ModelRequest.normalizeAiSdkOptions(packageID, options)
return { ...lowerer.request(request.body), ...request.generation, ...request.options }
}
const request = info.options && ingest(info.options)
const costs = info.cost && [ const costs = info.cost && [
{ {
input: info.cost.input, input: info.cost.input,
@@ -236,7 +241,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
info.variants && info.variants &&
Object.entries(info.variants).map(([id, options]) => ({ Object.entries(info.variants).map(([id, options]) => ({
id, id,
body: lowerer.request(options), body: ingest(options),
})), })),
cost: costs, cost: costs,
disabled: info.status === "deprecated" ? true : undefined, disabled: info.status === "deprecated" ? true : undefined,
+65 -2
View File
@@ -1,8 +1,71 @@
export * as PermissionV1 from "./permission" export * as PermissionV1 from "./permission"
import { Schema } from "effect" import { Schema } from "effect"
export * from "@opencode-ai/schema/permission-v1" import { ProjectV2 } from "../project"
import { ID } from "@opencode-ai/schema/permission-v1" import { withStatics } from "../schema"
import { SessionSchema } from "../session/schema"
import { Identifier } from "../util/identifier"
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
)
export type ID = typeof ID.Type
export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
export type Action = typeof Action.Type
export const Rule = Schema.Struct({
permission: Schema.String,
pattern: Schema.String,
action: Action,
}).annotate({ identifier: "PermissionRule" })
export type Rule = typeof Rule.Type
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
export type Ruleset = typeof Ruleset.Type
export const Request = Schema.Struct({
id: ID,
sessionID: SessionSchema.ID,
permission: Schema.String,
patterns: Schema.Array(Schema.String),
metadata: Schema.Record(Schema.String, Schema.Unknown),
always: Schema.Array(Schema.String),
tool: Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).pipe(Schema.optional),
}).annotate({ identifier: "PermissionRequest" })
export type Request = typeof Request.Type
export const Reply = Schema.Literals(["once", "always", "reject"])
export type Reply = typeof Reply.Type
export const ReplyBody = Schema.Struct({
reply: Reply,
message: Schema.String.pipe(Schema.optional),
}).annotate({ identifier: "PermissionReplyBody" })
export type ReplyBody = typeof ReplyBody.Type
export const Approval = Schema.Struct({
projectID: ProjectV2.ID,
patterns: Schema.Array(Schema.String),
}).annotate({ identifier: "PermissionApproval" })
export type Approval = typeof Approval.Type
export const AskInput = Schema.Struct({
...Request.fields,
id: ID.pipe(Schema.optional),
ruleset: Ruleset,
}).annotate({ identifier: "PermissionAskInput" })
export type AskInput = typeof AskInput.Type
export const ReplyInput = Schema.Struct({
requestID: ID,
...ReplyBody.fields,
}).annotate({ identifier: "PermissionReplyInput" })
export type ReplyInput = typeof ReplyInput.Type
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) { export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) {
override get message() { override get message() {
+607 -43
View File
@@ -1,52 +1,39 @@
export * as SessionV1 from "./session" export * as SessionV1 from "./session"
import { Schema } from "effect" import { Effect, Schema, Types } from "effect"
import { EventV2 } from "../event"
import { PermissionV1 } from "./permission"
import { ProjectV2 } from "../project"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
import { optionalOmitUndefined, withStatics } from "../schema"
import { Identifier } from "../util/identifier"
import { NonNegativeInt } from "../schema" import { NonNegativeInt } from "../schema"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { SessionSchema } from "../session/schema"
import { WorkspaceV2 } from "../workspace"
export { const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
AgentPart,
AgentPartInput, export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
Assistant, Schema.brand("MessageID"),
CompactionPart, withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + Identifier.ascending()) })),
Event, )
FilePart, export type MessageID = typeof MessageID.Type
FilePartInput,
FilePartSource, export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
FileSource, Schema.brand("PartID"),
Format, withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + Identifier.ascending()) })),
Info, )
MessageID, export type PartID = typeof PartID.Type
OutputFormatJsonSchema,
OutputFormatText,
Part,
PartID,
PatchPart,
Range,
ReasoningPart,
ResourceSource,
RetryPart,
SessionInfo,
SnapshotPart,
StepFinishPart,
StepStartPart,
SubtaskPart,
SubtaskPartInput,
SymbolSource,
TextPart,
TextPartInput,
ToolPart,
ToolState,
ToolStateCompleted,
ToolStateError,
ToolStatePending,
ToolStateRunning,
User,
WithParts,
} from "@opencode-ai/schema/session-v1"
export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) export const OutputLengthError = NamedError.create("MessageOutputLengthError", {})
export const AuthError = NamedError.create("ProviderAuthError", { providerID: Schema.String, message: Schema.String })
export const AuthError = NamedError.create("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = NamedError.create("StructuredOutputError", { export const StructuredOutputError = NamedError.create("StructuredOutputError", {
message: Schema.String, message: Schema.String,
@@ -65,4 +52,581 @@ export const ContextOverflowError = NamedError.create("ContextOverflowError", {
message: Schema.String, message: Schema.String,
responseBody: Schema.optional(Schema.String), responseBody: Schema.optional(Schema.String),
}) })
export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String }) export const ContentFilterError = NamedError.create("ContentFilterError", {
message: Schema.String,
})
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
type: Schema.Literal("text"),
}) {}
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
type: Schema.Literal("json_schema"),
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
}) {}
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
discriminator: "type",
identifier: "OutputFormat",
})
export type OutputFormat = Schema.Schema.Type<typeof Format>
const partBase = {
id: PartID,
sessionID: SessionSchema.ID,
messageID: MessageID,
}
export const SnapshotPart = Schema.Struct({
...partBase,
type: Schema.Literal("snapshot"),
snapshot: Schema.String,
}).annotate({ identifier: "SnapshotPart" })
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
export const PatchPart = Schema.Struct({
...partBase,
type: Schema.Literal("patch"),
hash: Schema.String,
files: Schema.Array(Schema.String),
}).annotate({ identifier: "PatchPart" })
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
export const TextPart = Schema.Struct({
...partBase,
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPart" })
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
export const ReasoningPart = Schema.Struct({
...partBase,
type: Schema.Literal("reasoning"),
text: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
}).annotate({ identifier: "ReasoningPart" })
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
const filePartSourceBase = {
text: Schema.Struct({
value: Schema.String,
start: Schema.Finite,
end: Schema.Finite,
}).annotate({ identifier: "FilePartSourceText" }),
}
export const Range = Schema.Struct({
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
}).annotate({ identifier: "Range" })
export type Range = typeof Range.Type
export const FileSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("file"),
path: Schema.String,
}).annotate({ identifier: "FileSource" })
export const SymbolSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("symbol"),
path: Schema.String,
range: Range,
name: Schema.String,
kind: NonNegativeInt,
}).annotate({ identifier: "SymbolSource" })
export const ResourceSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("resource"),
clientName: Schema.String,
uri: Schema.String,
}).annotate({ identifier: "ResourceSource" })
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
discriminator: "type",
identifier: "FilePartSource",
})
export const FilePart = Schema.Struct({
...partBase,
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePart" })
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
export const AgentPart = Schema.Struct({
...partBase,
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPart" })
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
export const CompactionPart = Schema.Struct({
...partBase,
type: Schema.Literal("compaction"),
auto: Schema.Boolean,
overflow: Schema.optional(Schema.Boolean),
tail_start_id: Schema.optional(MessageID),
}).annotate({ identifier: "CompactionPart" })
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
export const SubtaskPart = Schema.Struct({
...partBase,
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPart" })
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
export const RetryPart = Schema.Struct({
...partBase,
type: Schema.Literal("retry"),
attempt: NonNegativeInt,
error: APIError.EffectSchema,
time: Schema.Struct({
created: NonNegativeInt,
}),
}).annotate({ identifier: "RetryPart" })
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
error: APIError
}
export const StepStartPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-start"),
snapshot: Schema.optional(Schema.String),
}).annotate({ identifier: "StepStartPart" })
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
export const StepFinishPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-finish"),
reason: Schema.String,
snapshot: Schema.optional(Schema.String),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
}).annotate({ identifier: "StepFinishPart" })
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
export const ToolStatePending = Schema.Struct({
status: Schema.Literal("pending"),
input: Schema.Record(Schema.String, Schema.Any),
raw: Schema.String,
}).annotate({ identifier: "ToolStatePending" })
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
export const ToolStateRunning = Schema.Struct({
status: Schema.Literal("running"),
input: Schema.Record(Schema.String, Schema.Any),
title: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateRunning" })
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
export const ToolStateCompleted = Schema.Struct({
status: Schema.Literal("completed"),
input: Schema.Record(Schema.String, Schema.Any),
output: Schema.String,
title: Schema.String,
metadata: Schema.Record(Schema.String, Schema.Any),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
compacted: Schema.optional(NonNegativeInt),
}),
attachments: Schema.optional(Schema.Array(FilePart)),
}).annotate({ identifier: "ToolStateCompleted" })
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
export const ToolStateError = Schema.Struct({
status: Schema.Literal("error"),
input: Schema.Record(Schema.String, Schema.Any),
error: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateError" })
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
export const ToolState = Schema.Union([
ToolStatePending,
ToolStateRunning,
ToolStateCompleted,
ToolStateError,
]).annotate({
discriminator: "status",
identifier: "ToolState",
})
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export const ToolPart = Schema.Struct({
...partBase,
type: Schema.Literal("tool"),
callID: Schema.String,
tool: Schema.String,
state: ToolState,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "ToolPart" })
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
state: ToolState
}
const messageBase = {
id: MessageID,
sessionID: partBase.sessionID,
}
const FileDiff = Schema.Struct({
file: Schema.optional(Schema.String),
patch: Schema.optional(Schema.String),
additions: Schema.Finite,
deletions: Schema.Finite,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
}).annotate({ identifier: "SnapshotFileDiff" })
export const User = Schema.Struct({
...messageBase,
role: Schema.Literal("user"),
time: Schema.Struct({
created: Timestamp,
}),
format: Schema.optional(Format),
summary: Schema.optional(
Schema.Struct({
title: Schema.optional(Schema.String),
body: Schema.optional(Schema.String),
diffs: Schema.Array(FileDiff),
}),
),
agent: Schema.String,
model: Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: Schema.optional(Schema.String),
}),
system: Schema.optional(Schema.String),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
}).annotate({ identifier: "UserMessage" })
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
export const Part = Schema.Union([
TextPart,
SubtaskPart,
ReasoningPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
RetryPart,
CompactionPart,
]).annotate({ discriminator: "type", identifier: "Part" })
export type Part =
| TextPart
| SubtaskPart
| ReasoningPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| PatchPart
| AgentPart
| RetryPart
| CompactionPart
const AssistantErrorSchema = Schema.Union([
AuthError.EffectSchema,
NamedError.Unknown.EffectSchema,
OutputLengthError.EffectSchema,
AbortedError.EffectSchema,
StructuredOutputError.EffectSchema,
ContextOverflowError.EffectSchema,
ContentFilterError.EffectSchema,
APIError.EffectSchema,
]).annotate({ discriminator: "name" })
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
export const TextPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPartInput" })
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
export const FilePartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePartInput" })
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
export const AgentPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPartInput" })
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
export const SubtaskPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPartInput" })
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
export const Assistant = Schema.Struct({
...messageBase,
role: Schema.Literal("assistant"),
time: Schema.Struct({
created: NonNegativeInt,
completed: Schema.optional(NonNegativeInt),
}),
error: Schema.optional(AssistantErrorSchema),
parentID: MessageID,
modelID: ModelV2.ID,
providerID: ProviderV2.ID,
mode: Schema.String,
agent: Schema.String,
path: Schema.Struct({
cwd: Schema.String,
root: Schema.String,
}),
summary: Schema.optional(Schema.Boolean),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
structured: Schema.optional(Schema.Any),
variant: Schema.optional(Schema.String),
finish: Schema.optional(Schema.String),
}).annotate({ identifier: "AssistantMessage" })
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
error?: AssistantError
}
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
export type Info = User | Assistant
export const WithParts = Schema.Struct({
info: Info,
parts: Schema.Array(Part),
})
export type WithParts = {
info: Info
parts: Part[]
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const SessionSummary = Schema.Struct({
additions: Schema.Finite,
deletions: Schema.Finite,
files: Schema.Finite,
diffs: optionalOmitUndefined(Schema.Array(FileDiff)),
})
const SessionTokens = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
})
const SessionShare = Schema.Struct({
url: Schema.String,
})
const SessionRevert = Schema.Struct({
messageID: MessageID,
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
})
const SessionModel = Schema.Struct({
id: ModelV2.ID,
providerID: ProviderV2.ID,
variant: optionalOmitUndefined(Schema.String),
})
export const SessionInfo = Schema.Struct({
id: SessionSchema.ID,
slug: Schema.String,
projectID: ProjectV2.ID,
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
directory: Schema.String,
path: optionalOmitUndefined(Schema.String),
parentID: optionalOmitUndefined(SessionSchema.ID),
summary: optionalOmitUndefined(SessionSummary),
cost: optionalOmitUndefined(Schema.Finite),
tokens: optionalOmitUndefined(SessionTokens),
share: optionalOmitUndefined(SessionShare),
title: Schema.String,
agent: optionalOmitUndefined(Schema.String),
model: optionalOmitUndefined(SessionModel),
version: Schema.String,
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
created: NonNegativeInt,
updated: NonNegativeInt,
compacting: optionalOmitUndefined(NonNegativeInt),
archived: optionalOmitUndefined(Schema.Finite),
}),
permission: optionalOmitUndefined(PermissionV1.Ruleset),
revert: optionalOmitUndefined(SessionRevert),
}).annotate({ identifier: "Session" })
export type SessionInfo = typeof SessionInfo.Type
export const Event = {
Created: EventV2.define({
type: "session.created",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Updated: EventV2.define({
type: "session.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Deleted: EventV2.define({
type: "session.deleted",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
MessageUpdated: EventV2.define({
type: "message.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: Info,
},
}),
MessageRemoved: EventV2.define({
type: "message.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
},
}),
PartUpdated: EventV2.define({
type: "message.part.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
part: Part,
time: Schema.Finite,
},
}),
PartRemoved: EventV2.define({
type: "message.part.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
partID: PartID,
},
}),
}
+5 -2
View File
@@ -233,13 +233,16 @@ describe("CatalogV2", () => {
model.request.headers.shared = "model" model.request.headers.shared = "model"
model.request.body.model = true model.request.body.model = true
model.request.body.request = true model.request.body.request = true
model.request.body.shared = "model" const options = (model.request.options ??= {})
options.shared = "model"
options.model = true
}) })
}) })
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" }) expect(model.request.body).toEqual({ provider: true, model: true, request: true })
expect(model.request.options).toEqual({ shared: "model", model: true })
}), }),
) )
+2 -2
View File
@@ -599,9 +599,9 @@ describe("Config", () => {
models: { models: {
model: { model: {
request: { request: {
body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" }, body: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
}, },
variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }], variants: [{ id: "high", body: { reasoningEffort: "high", reasoningSummary: "auto" } }],
}, },
}, },
}) })
+6 -4
View File
@@ -55,7 +55,7 @@ function request(headers: Record<string, string>, variant?: string) {
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigProviderPlugin.Plugin", () => { describe("ConfigProviderPlugin.Plugin", () => {
it.effect("keeps configured model variant bodies unchanged", () => it.effect("partitions existing model variant bodies without changing config shape", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode const providerID = ProviderV2.ID.opencode
@@ -96,7 +96,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
expect(model.variants).toMatchObject([ expect(model.variants).toMatchObject([
{ {
id: "high", id: "high",
body: { body: {},
options: {
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
@@ -106,7 +107,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
}), }),
) )
it.effect("keeps layered model variant bodies unchanged", () => it.effect("uses the effective provider package across layered config", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode const providerID = ProviderV2.ID.opencode
@@ -146,7 +147,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({ expect(model.variants[0]).toMatchObject({
id: "high", id: "high",
body: { reasoningEffort: "high" }, body: {},
options: { reasoningEffort: "high" },
}) })
}), }),
) )
+138 -126
View File
@@ -1,14 +1,10 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Event } from "@opencode-ai/schema/event"
import { Session } from "@opencode-ai/schema/session"
import { SessionEvent } from "@opencode-ai/schema/session-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import { Database } from "@opencode-ai/core/database/database" import { Database } from "@opencode-ai/core/database/database"
import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/core/schema"
import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { location } from "./fixture/location" import { location } from "./fixture/location"
@@ -20,6 +16,10 @@ const locationLayer = Layer.succeed(
location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }), location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
), ),
) )
const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
const Message = EventV2.define({ const Message = EventV2.define({
type: "test.message", type: "test.message",
schema: { schema: {
@@ -70,16 +70,18 @@ const VersionedMessage = EventV2.define({
}, },
}) })
const DurableMessage = SessionV1.Event.MessageRemoved const SyncTimestamp = EventV2.define({
const durableData = (sessionID: Session.ID, text: string) => ({ type: "test.timestamp",
sessionID, durable: {
messageID: SessionV1.MessageID.ascending(`msg_${text}`), version: 1,
aggregate: "id",
},
schema: {
id: Schema.String,
timestamp: DateTimeUtcFromMillis,
},
}) })
const eventLayer = Layer.mergeAll(EventV2.layerWith().pipe(Layer.provide(Database.defaultLayer)), Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
describe("EventV2", () => { describe("EventV2", () => {
it.effect("publishes events with the current location", () => it.effect("publishes events with the current location", () =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -120,21 +122,26 @@ describe("EventV2", () => {
}), }),
) )
it.effect("selects the latest durable definition independent of declaration order", () => it.effect("stores definitions in the exported registry", () =>
Effect.sync(() => {
expect(EventV2.registry.get(Message.type)).toBe(Message)
}),
)
it.effect("keeps the latest sync definition in the registry", () =>
Effect.sync(() => { Effect.sync(() => {
const latest = EventV2.define({ const latest = EventV2.define({
type: "test.out-of-order", type: "test.out-of-order",
durable: { version: 2, aggregate: "id" }, durable: { version: 2, aggregate: "id" },
schema: { id: Schema.String }, schema: { id: Schema.String },
}) })
const historical = EventV2.define({ EventV2.define({
type: "test.out-of-order", type: "test.out-of-order",
durable: { version: 1, aggregate: "id" }, durable: { version: 1, aggregate: "id" },
schema: { id: Schema.String }, schema: { id: Schema.String },
}) })
expect(Event.latest([latest, historical]).get("test.out-of-order")).toBe(latest) expect(EventV2.registry.get("test.out-of-order")).toBe(latest)
expect(Event.latest([historical, latest]).get("test.out-of-order")).toBe(latest)
}), }),
) )
@@ -356,19 +363,19 @@ describe("EventV2", () => {
it.effect("replays durable aggregate events after a sequence and tails new events", () => it.effect("replays durable aggregate events after a sequence and tails new events", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
yield* events.publish(DurableMessage, durableData(aggregateID, "one")) yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
const fiber = yield* events const fiber = yield* events
.durable({ aggregateID, after: 0 }) .durable({ aggregateID, after: 0 })
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
yield* events.publish(DurableMessage, durableData(aggregateID, "two")) yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[1, durableData(aggregateID, "one")], [1, { id: aggregateID, text: "one" }],
[2, durableData(aggregateID, "two")], [2, { id: aggregateID, text: "two" }],
]) ])
}), }),
) )
@@ -376,15 +383,20 @@ describe("EventV2", () => {
it.effect("catches durable aggregate events published during replay handoff", () => it.effect("catches durable aggregate events published during replay handoff", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* events.publish(DurableMessage, durableData(aggregateID, "one")) yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ expect(
[0, durableData(aggregateID, "zero")], Array.from(yield* Fiber.join(fiber)).map((event) => [
[1, durableData(aggregateID, "one")], event.durable?.seq,
(event.data as { text: string }).text,
]),
).toEqual([
[0, "zero"],
[1, "one"],
]) ])
}), }),
) )
@@ -403,16 +415,16 @@ describe("EventV2", () => {
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Deferred.await(readStarted) yield* Deferred.await(readStarted)
pause = false pause = false
yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff")) yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" })
yield* Deferred.succeed(continueRead, undefined) yield* Deferred.succeed(continueRead, undefined)
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[0, durableData(aggregateID, "during handoff")], [0, { id: aggregateID, text: "during handoff" }],
]) ])
}).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer))) }).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
}), }),
@@ -421,7 +433,7 @@ describe("EventV2", () => {
it.effect("coalesces durable aggregate wakes while draining every committed event", () => it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const count = 64 const count = 64
const fiber = yield* events const fiber = yield* events
.durable({ aggregateID }) .durable({ aggregateID })
@@ -429,11 +441,11 @@ describe("EventV2", () => {
yield* Effect.yieldNow yield* Effect.yieldNow
for (let index = 0; index < count; index++) { for (let index = 0; index < count; index++) {
yield* events.publish(DurableMessage, durableData(aggregateID, String(index))) yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) })
} }
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual( expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]), Array.from({ length: count }, (_, index) => [index, { id: aggregateID, text: String(index) }]),
) )
}), }),
) )
@@ -441,14 +453,14 @@ describe("EventV2", () => {
it.effect("omits live-only events from durable aggregate streams", () => it.effect("omits live-only events from durable aggregate streams", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
yield* events.publish(Message, { text: "live only" }) yield* events.publish(Message, { text: "live only" })
yield* events.publish(DurableMessage, durableData(aggregateID, "durable")) yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type]) expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([SyncMessage.type])
}), }),
) )
@@ -475,23 +487,23 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.project(DurableMessage, (event) => yield* events.project(SyncMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
) )
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "hello"), data: { id: aggregateID, text: "hello" },
}) })
expect(received[0]?.type).toBe(DurableMessage.type) expect(received[0]?.type).toBe(SyncMessage.type)
expect(received[0]?.data).toEqual(durableData(aggregateID, "hello")) expect(received[0]?.data).toEqual({ id: aggregateID, text: "hello" })
}), }),
) )
@@ -499,14 +511,14 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "replayed"), data: { id: aggregateID, text: "replayed" },
}) })
const rows = yield* db const rows = yield* db
.select() .select()
@@ -526,11 +538,11 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const envelopeAggregateID = Session.ID.create() const envelopeAggregateID = EventV2.ID.create()
const payloadAggregateID = Session.ID.create() const payloadAggregateID = EventV2.ID.create()
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed")) yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" })
yield* events.project(DurableMessage, (event) => yield* events.project(SyncMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -539,10 +551,10 @@ describe("EventV2", () => {
const exit = yield* events const exit = yield* events
.replay({ .replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID: envelopeAggregateID, aggregateID: envelopeAggregateID,
data: durableData(payloadAggregateID, "replayed"), data: { id: payloadAggregateID, text: "replayed" },
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
const rows = yield* db const rows = yield* db
@@ -568,22 +580,22 @@ describe("EventV2", () => {
it.effect("replay defects on sequence mismatch", () => it.effect("replay defects on sequence mismatch", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "first"), data: { id: aggregateID, text: "first" },
}) })
const exit = yield* events const exit = yield* events
.replay({ .replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 5, seq: 5,
aggregateID, aggregateID,
data: durableData(aggregateID, "bad"), data: { id: aggregateID, text: "bad" },
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -594,9 +606,9 @@ describe("EventV2", () => {
it.effect("replay decodes synchronized transformed values before projection", () => it.effect("replay decodes synchronized transformed values before projection", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const received = new Array<typeof SessionEvent.ContextUpdated.Type>() const received = new Array<typeof SyncTimestamp.Type>()
yield* events.project(SessionEvent.ContextUpdated, (event) => yield* events.project(SyncTimestamp, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -604,10 +616,10 @@ describe("EventV2", () => {
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1), type: EventV2.versionedType(SyncTimestamp.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: { sessionID: aggregateID, messageID: "msg_context", timestamp: 0, text: "context" }, data: { id: aggregateID, timestamp: 0 },
}) })
expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0)) expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0))
@@ -634,21 +646,21 @@ describe("EventV2", () => {
it.effect("replayAll validates contiguous aggregate events", () => it.effect("replayAll validates contiguous aggregate events", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const source = yield* events.replayAll([ const source = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "one"), data: { id: aggregateID, text: "one" },
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "two"), data: { id: aggregateID, text: "two" },
}, },
]) ])
@@ -660,38 +672,38 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const one = yield* events.replayAll([ const one = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "one"), data: { id: aggregateID, text: "one" },
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "two"), data: { id: aggregateID, text: "two" },
}, },
]) ])
const two = yield* events.replayAll([ const two = yield* events.replayAll([
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 2, seq: 2,
aggregateID, aggregateID,
data: durableData(aggregateID, "three"), data: { id: aggregateID, text: "three" },
}, },
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 3, seq: 3,
aggregateID, aggregateID,
data: durableData(aggregateID, "four"), data: { id: aggregateID, text: "four" },
}, },
]) ])
const rows = yield* db const rows = yield* db
@@ -711,10 +723,10 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" })
yield* events.claim(aggregateID, "owner-a") yield* events.claim(aggregateID, "owner-a")
yield* events.project(DurableMessage, (event) => yield* events.project(SyncMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -723,10 +735,10 @@ describe("EventV2", () => {
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "ignored"), data: { id: aggregateID, text: "ignored" },
}, },
{ ownerID: "owner-b" }, { ownerID: "owner-b" },
) )
@@ -738,14 +750,14 @@ describe("EventV2", () => {
it.effect("strict owner fences exact replay", () => it.effect("strict owner fences exact replay", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const id = EventV2.ID.create() const id = EventV2.ID.create()
const replayed = { const replayed = {
id, id,
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "owned"), data: { id: aggregateID, text: "owned" },
} }
yield* events.replay(replayed, { ownerID: "owner-a" }) yield* events.replay(replayed, { ownerID: "owner-a" })
@@ -759,11 +771,11 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned")) const published = yield* events.publish(SyncMessage, { id: aggregateID, text: "owned" })
const replayed = { const replayed = {
id: published.id, id: published.id,
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: published.durable!.seq, seq: published.durable!.seq,
aggregateID, aggregateID,
data: published.data, data: published.data,
@@ -780,7 +792,7 @@ describe("EventV2", () => {
expect(row?.ownerID).toBe("owner-a") expect(row?.ownerID).toBe("owner-a")
const exit = yield* events const exit = yield* events
.replay( .replay(
{ ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") }, { ...replayed, id: EventV2.ID.create(), seq: 1, data: { id: aggregateID, text: "conflict" } },
{ ownerID: "owner-b", strictOwner: true }, { ownerID: "owner-b", strictOwner: true },
) )
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -792,15 +804,15 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "owned"), data: { id: aggregateID, text: "owned" },
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
@@ -819,26 +831,26 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "local")) yield* events.publish(SyncMessage, { id: aggregateID, text: "local" })
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "claimed"), data: { id: aggregateID, text: "claimed" },
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 2, seq: 2,
aggregateID, aggregateID,
data: durableData(aggregateID, "fenced"), data: { id: aggregateID, text: "fenced" },
}, },
{ ownerID: "owner-2" }, { ownerID: "owner-2" },
) )
@@ -863,14 +875,14 @@ describe("EventV2", () => {
it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => it.effect("strict replay rejects an owner conflict instead of silently skipping it", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "claimed"), data: { id: aggregateID, text: "claimed" },
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
@@ -879,10 +891,10 @@ describe("EventV2", () => {
.replay( .replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "conflict"), data: { id: aggregateID, text: "conflict" },
}, },
{ ownerID: "owner-2", strictOwner: true }, { ownerID: "owner-2", strictOwner: true },
) )
@@ -896,14 +908,14 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
const replayed = { const replayed = {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "replayed"), data: { id: aggregateID, text: "replayed" },
} }
yield* events.replay(replayed, { publish: true }) yield* events.replay(replayed, { publish: true })
@@ -917,19 +929,19 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const replayed = { const replayed = {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "original"), data: { id: aggregateID, text: "original" },
} }
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay(replayed, { publish: true }) yield* events.replay(replayed, { publish: true })
const exit = yield* events const exit = yield* events
.replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true }) .replay({ ...replayed, data: { id: aggregateID, text: "divergent" } }, { publish: true })
.pipe(Effect.exit) .pipe(Effect.exit)
expect(String(exit)).toContain("Replay diverged") expect(String(exit)).toContain("Replay diverged")
@@ -940,23 +952,23 @@ describe("EventV2", () => {
it.effect("rejects an event ID reused at another aggregate position", () => it.effect("rejects an event ID reused at another aggregate position", () =>
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const id = EventV2.ID.create() const id = EventV2.ID.create()
yield* events.replay({ yield* events.replay({
id, id,
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "first"), data: { id: aggregateID, text: "first" },
}) })
const exit = yield* events const exit = yield* events
.replay({ .replay({
id, id,
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "second"), data: { id: aggregateID, text: "second" },
}) })
.pipe(Effect.exit) .pipe(Effect.exit)
@@ -968,27 +980,27 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const { db } = yield* Database.Service const { db } = yield* Database.Service
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
yield* events.listen((event) => Effect.sync(() => received.push(event))) yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "first"), data: { id: aggregateID, text: "first" },
}, },
{ ownerID: "owner-1" }, { ownerID: "owner-1" },
) )
yield* events.replay( yield* events.replay(
{ {
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1, seq: 1,
aggregateID, aggregateID,
data: durableData(aggregateID, "ignored"), data: { id: aggregateID, text: "ignored" },
}, },
{ ownerID: "owner-2", publish: true }, { ownerID: "owner-2", publish: true },
) )
@@ -1035,10 +1047,10 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>() const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create() const aggregateID = EventV2.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" })
yield* events.remove(aggregateID) yield* events.remove(aggregateID)
yield* events.project(DurableMessage, (event) => yield* events.project(SyncMessage, (event) =>
Effect.sync(() => { Effect.sync(() => {
received.push(event) received.push(event)
}), }),
@@ -1046,13 +1058,13 @@ describe("EventV2", () => {
yield* events.replay({ yield* events.replay({
id: EventV2.ID.create(), id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1), type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0, seq: 0,
aggregateID, aggregateID,
data: durableData(aggregateID, "replayed"), data: { id: aggregateID, text: "replayed" },
}) })
expect(received[0]?.data).toEqual(durableData(aggregateID, "replayed")) expect(received[0]?.data).toEqual({ id: aggregateID, text: "replayed" })
}), }),
) )
}) })
@@ -1,16 +0,0 @@
import { describe, expect, test } from "bun:test"
import { SessionV1 as Wire } from "@opencode-ai/schema/session-v1"
import { SessionV1 } from "../src/v1/session"
describe("legacy event schema compatibility", () => {
test("Core references canonical SessionV1 definitions", () => {
expect(SessionV1.Event.Created).toBe(Wire.Event.Created)
expect(SessionV1.Event.PartUpdated).toBe(Wire.Event.PartUpdated)
})
test("Core retains NamedError constructor identity", () => {
const error = new SessionV1.APIError({ message: "failed", isRetryable: false })
expect(error).toBeInstanceOf(SessionV1.APIError)
expect(error.toObject()).toEqual({ name: "APIError", data: { message: "failed", isRetryable: false } })
})
})
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test"
import { ModelRequest } from "@opencode-ai/core/model-request"
describe("ModelRequest", () => {
test("partitions AI SDK model and models.dev mode options", () => {
expect(
ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", {
maxOutputTokens: 4096,
temperature: 0.2,
reasoningEffort: "high",
serviceTier: "priority",
custom_extension: { enabled: true },
}),
).toEqual({
generation: { maxTokens: 4096, temperature: 0.2 },
options: { reasoningEffort: "high", serviceTier: "priority" },
body: { custom_extension: { enabled: true } },
})
})
test("keeps unknown-provider options as compatibility fields", () => {
expect(ModelRequest.normalizeAiSdkOptions(undefined, { temperature: 0.2, reasoningEffort: "high" })).toEqual({
generation: { temperature: 0.2 },
options: {},
body: { reasoningEffort: "high" },
})
})
test("does not consult inherited package-name properties", () => {
expect(ModelRequest.normalizeAiSdkOptions("__proto__", { reasoningEffort: "high" })).toEqual({
generation: {},
options: {},
body: { reasoningEffort: "high" },
})
})
test("normalizes models.dev wire aliases owned by native protocols", () => {
expect(ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", { service_tier: "priority" })).toEqual({
generation: {},
options: { serviceTier: "priority" },
body: {},
})
})
})
+10
View File
@@ -290,11 +290,21 @@ function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
...value.request, ...value.request,
headers: { ...value.request.headers }, headers: { ...value.request.headers },
body: { ...value.request.body }, body: { ...value.request.body },
generation: value.request.generation && {
...value.request.generation,
stop: value.request.generation.stop && [...value.request.generation.stop],
},
options: value.request.options && { ...value.request.options },
}, },
variants: value.variants.map((variant) => ({ variants: value.variants.map((variant) => ({
...variant, ...variant,
headers: { ...variant.headers }, headers: { ...variant.headers },
body: { ...variant.body }, body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
})), })),
time: { ...value.time }, time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })), cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
@@ -150,12 +150,15 @@ describe("OpencodePlugin", () => {
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 }, limit: { context: 1000, output: 100 },
}) })
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 }) expect(model.request).toMatchObject({ body: { custom: "value" }, generation: { temperature: 0.5 } })
expect(model.request.body).toEqual({ custom: "value" })
expect(model.variants).toEqual([ expect(model.variants).toEqual([
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: { temperature: 0.2 }, body: {},
generation: { temperature: 0.2 },
options: {},
}, },
]) ])
expect( expect(
-79
View File
@@ -1,79 +0,0 @@
import { describe, expect } from "bun:test"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event"
import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { Policy } from "@opencode-ai/core/policy"
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
)
const connections = Credential.defaultLayer.pipe(Layer.fresh)
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
const catalog = Catalog.layer.pipe(
Layer.provide(
Layer.mergeAll(events, locationLayer, Policy.layer.pipe(Layer.provide(locationLayer)), connections, integrations),
),
)
const it = testEffect(
Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer),
)
describe("VariantPlugin", () => {
it.effect("adds GLM 5.2 variants after catalog sources", () =>
Effect.gen(function* () {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
})
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.api = {
id: ModelV2.ID.make("glm-5.2"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", body: { reasoning_effort: "high" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
])
}),
)
it.effect("keeps explicit variants over generated defaults", () =>
Effect.gen(function* () {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.api = {
id: ModelV2.ID.make("glm-5.2"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }]
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
])
}),
)
})
+36 -21
View File
@@ -31,6 +31,8 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
request: { request: {
headers: { "x-test": "header" }, headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } }, body: { apiKey: "secret", custom_extension: { enabled: true } },
generation: { temperature: 0.7 },
options: { store: false, serviceTier: "priority" },
}, },
variants, variants,
time: { released: 0 }, time: { released: 0 },
@@ -54,6 +56,8 @@ describe("SessionRunnerModel", () => {
defaults: { defaults: {
headers: { "x-test": "header" }, headers: { "x-test": "header" },
limits: { context: 100, output: 20 }, limits: { context: 100, output: 20 },
generation: { temperature: 0.7 },
providerOptions: { openai: { store: false, serviceTier: "priority" } },
http: { body: { custom_extension: { enabled: true } } }, http: { body: { custom_extension: { enabled: true } } },
}, },
}) })
@@ -82,7 +86,7 @@ describe("SessionRunnerModel", () => {
url: "https://compatible.example/v1", url: "https://compatible.example/v1",
settings: { apiKey: "settings-secret", compatibility: "strict" }, settings: { apiKey: "settings-secret", compatibility: "strict" },
}), }),
request: { headers: {}, body: {} }, request: { headers: {}, body: {}, generation: {}, options: {} },
}), }),
) )
const request = LLM.request({ model: resolved, prompt: "Hello" }) const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -99,20 +103,21 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("overlays selected OpenAI Session variant bodies", () => it.effect("lowers selected OpenAI Session variants into Responses options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [ const base = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: { "x-variant": "high" }, headers: { "x-variant": "high" },
body: { body: {},
store: false, generation: { temperature: 0.2 },
service_tier: "priority", options: { reasoningEffort: "high" },
temperature: 0.2,
reasoning: { effort: "high" },
},
}, },
]) ])
const catalog = ModelV2.Info.make({
...base,
request: { ...base.request, options: { ...base.request.options, reasoningEffort: "medium" } },
})
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant"), id: SessionV2.ID.make("ses_model_variant"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@@ -129,19 +134,21 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
expect(resolved.route.defaults.http?.body).toEqual({ expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
custom_extension: { enabled: true }, expect(prepared.body).toMatchObject({
store: false, store: false,
service_tier: "priority", service_tier: "priority",
temperature: 0.2, temperature: 0.2,
reasoning: { effort: "high" }, reasoning: { effort: "high" },
}) })
expect(prepared.body).not.toHaveProperty("reasoningEffort")
}), }),
) )
it.effect("overlays selected OpenAI-compatible Session variant bodies", () => it.effect("lowers selected OpenAI-compatible Session variants into Chat options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model( const catalog = model(
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" }, { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
@@ -149,7 +156,9 @@ describe("SessionRunnerModel", () => {
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: { store: false, reasoning_effort: "high" }, body: {},
generation: {},
options: { reasoningEffort: "high" },
}, },
], ],
) )
@@ -165,12 +174,14 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.http?.body).toEqual({ expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
custom_extension: { enabled: true }, expect(prepared.body).toMatchObject({
store: false, store: false,
reasoning_effort: "high", reasoning_effort: "high",
}) })
expect(prepared.body).not.toHaveProperty("reasoningEffort")
}), }),
) )
@@ -204,13 +215,15 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("overlays selected Anthropic Session variant bodies", () => it.effect("lowers selected Anthropic Session variants into Messages options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [ const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
headers: {}, headers: {},
body: { thinking: { type: "enabled", budget_tokens: 12000 } }, body: {},
generation: {},
options: { thinking: { type: "enabled", budgetTokens: 12000 } },
}, },
]) ])
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
@@ -225,11 +238,13 @@ describe("SessionRunnerModel", () => {
}) })
const resolved = yield* SessionRunnerModel.resolve(session, catalog) const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.http?.body).toEqual({ expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
custom_extension: { enabled: true }, expect(prepared.body).toMatchObject({
thinking: { type: "enabled", budget_tokens: 12000 }, thinking: { type: "enabled", budget_tokens: 12000 },
}) })
expect(JSON.stringify(prepared.body)).not.toContain("budgetTokens")
}), }),
) )
@@ -251,7 +266,7 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: {} }, request: { headers: {}, body: {}, generation: {}, options: {} },
}), }),
Credential.Key.make({ type: "key", key: "secret" }), Credential.Key.make({ type: "key", key: "secret" }),
) )
@@ -274,7 +289,7 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: { apiKey: "configured-secret" } }, request: { headers: {}, body: { apiKey: "configured-secret" }, generation: {}, options: {} },
}), }),
credential, credential,
) )
+5
View File
@@ -19,6 +19,7 @@ import { Credential } from "@opencode-ai/schema/credential"
import { FileSystem } from "@opencode-ai/schema/filesystem" import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Integration } from "@opencode-ai/schema/integration" import { Integration } from "@opencode-ai/schema/integration"
import { LLM } from "@opencode-ai/schema/llm" import { LLM } from "@opencode-ai/schema/llm"
import { ModelRequest } from "@opencode-ai/schema/model-request"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { Reference } from "@opencode-ai/schema/reference" import { Reference } from "@opencode-ai/schema/reference"
import { Skill } from "@opencode-ai/schema/skill" import { Skill } from "@opencode-ai/schema/skill"
@@ -34,6 +35,7 @@ test("Core reuses the canonical shared schemas", async () => {
coreIntegration, coreIntegration,
coreLocation, coreLocation,
coreLLM, coreLLM,
coreModelRequest,
corePermission, corePermission,
coreProject, coreProject,
coreReference, coreReference,
@@ -51,6 +53,7 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/integration"), import("@opencode-ai/core/integration"),
import("@opencode-ai/core/location"), import("@opencode-ai/core/location"),
import("@opencode-ai/llm"), import("@opencode-ai/llm"),
import("@opencode-ai/core/model-request"),
import("@opencode-ai/core/permission"), import("@opencode-ai/core/permission"),
import("@opencode-ai/core/project/schema"), import("@opencode-ai/core/project/schema"),
import("@opencode-ai/core/reference"), import("@opencode-ai/core/reference"),
@@ -108,6 +111,8 @@ test("Core reuses the canonical shared schemas", async () => {
[ProviderV2.Api, Provider.Api], [ProviderV2.Api, Provider.Api],
[ProviderV2.Request, Provider.Request], [ProviderV2.Request, Provider.Request],
[ProviderV2.Info, Provider.Info], [ProviderV2.Info, Provider.Info],
[coreModelRequest.Generation, ModelRequest.Generation],
[coreModelRequest.Request, ModelRequest.Request],
[corePermission.Effect, Permission.Effect], [corePermission.Effect, Permission.Effect],
[corePermission.Rule, Permission.Rule], [corePermission.Rule, Permission.Rule],
[corePermission.Ruleset, Permission.Ruleset], [corePermission.Ruleset, Permission.Ruleset],
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.17.10", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.17.10", "version": "1.17.9",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -14,7 +14,6 @@
}, },
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
-1
View File
@@ -1,2 +1 @@
@import "@opencode-ai/ui/styles/tailwind"; @import "@opencode-ai/ui/styles/tailwind";
@import "@opencode-ai/session-ui/styles";
@@ -1,7 +1,7 @@
import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/session-ui/session-turn" import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/session-ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/session-ui/context" import { DataProvider } from "@opencode-ai/ui/context"
import { FileComponentProvider } from "@opencode-ai/ui/context/file" import { FileComponentProvider } from "@opencode-ai/ui/context/file"
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool" import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
import { createAsync, query, useParams } from "@solidjs/router" import { createAsync, query, useParams } from "@solidjs/router"
@@ -17,15 +17,15 @@ import { DateTime } from "luxon"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import NotFound from "../[...404]" import NotFound from "../[...404]"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { MessageNav } from "@opencode-ai/session-ui/message-nav" import { MessageNav } from "@opencode-ai/ui/message-nav"
import { FileSSR } from "@opencode-ai/session-ui/file-ssr" import { FileSSR } from "@opencode-ai/ui/file-ssr"
import { clientOnly } from "@solidjs/start" import { clientOnly } from "@solidjs/start"
import { Meta, Title } from "@solidjs/meta" import { Meta, Title } from "@solidjs/meta"
import { Base64 } from "js-base64" import { Base64 } from "js-base64"
import { getRequestEvent } from "solid-js/web" import { getRequestEvent } from "solid-js/web"
const ClientOnlyWorkerPoolProvider = clientOnly(() => const ClientOnlyWorkerPoolProvider = clientOnly(() =>
import("@opencode-ai/session-ui/pierre/worker").then((m) => ({ import("@opencode-ai/ui/pierre/worker").then((m) => ({
default: (props: { children: any }) => ( default: (props: { children: any }) => (
<WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider> <WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
), ),
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.17.10", "version": "1.17.9",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module", "type": "module",
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10", "version": "1.17.9",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -86,7 +86,6 @@
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*", "@opencode-ai/server": "workspace:*",
+11 -2
View File
@@ -2,20 +2,29 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge" import { EffectBridge } from "@/effect/bridge"
import type { InstanceContext } from "@/project/instance-context" import type { InstanceContext } from "@/project/instance-context"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { Skill } from "../skill" import { Skill } from "../skill"
import { EventV2 } from "@opencode-ai/core/event"
import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt" import PROMPT_REVIEW from "./template/review.txt"
import { LegacyEvent } from "@opencode-ai/schema/legacy-event"
type State = { type State = {
commands: Record<string, Info> commands: Record<string, Info>
} }
export const Event = { export const Event = {
Executed: LegacyEvent.CommandExecuted, Executed: EventV2.define({
type: "command.executed",
schema: {
name: Schema.String,
sessionID: SessionID,
arguments: Schema.String,
messageID: MessageID,
},
}),
} }
export const Info = Schema.Struct({ export const Info = Schema.Struct({
@@ -33,7 +33,6 @@ import { Vcs } from "@/project/vcs"
import { InstanceStore } from "@/project/instance-store" import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceBootstrap } from "@/project/bootstrap"
import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime" import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime"
import { WorkspaceEvent } from "@opencode-ai/schema/workspace-event"
export const Info = Schema.Struct({ export const Info = Schema.Struct({
...WorkspaceInfoSchema.fields, ...WorkspaceInfoSchema.fields,
@@ -41,10 +40,27 @@ export const Info = Schema.Struct({
}).annotate({ identifier: "Workspace" }) }).annotate({ identifier: "Workspace" })
export type Info = WorkspaceInfo & { timeUsed: number } export type Info = WorkspaceInfo & { timeUsed: number }
export const ConnectionStatus = WorkspaceEvent.ConnectionStatus export const ConnectionStatus = Schema.Struct({
export type ConnectionStatus = WorkspaceEvent.ConnectionStatus workspaceID: WorkspaceV2.ID,
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
})
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
export const Event = WorkspaceEvent export const Event = {
Ready: EventV2.define({
type: "workspace.ready",
schema: {
name: Schema.String,
},
}),
Failed: EventV2.define({
type: "workspace.failed",
schema: {
message: Schema.String,
},
}),
Status: EventV2.define({ type: "workspace.status", schema: ConnectionStatus.fields }),
}
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return { return {
-3
View File
@@ -1,3 +0,0 @@
export * as EventManifest from "./event-manifest"
export { Definitions, Durable, Latest } from "@opencode-ai/schema/event-manifest"
+8 -2
View File
@@ -7,6 +7,9 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import "@opencode-ai/core/account"
import "@opencode-ai/core/catalog"
import "@opencode-ai/core/session/event"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {} export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {}
@@ -42,7 +45,10 @@ export const layer = Layer.effect(
workspace: workspaceID, workspace: workspaceID,
payload: { id: event.id, type: event.type, properties: event.data }, payload: { id: event.id, type: event.type, properties: event.data },
}) })
if (event.durable === undefined) return const durable = EventV2.registry.get(event.type)?.durable
if (durable === undefined || event.durable === undefined) return
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
if (typeof aggregateID !== "string") return
GlobalBus.emit("event", { GlobalBus.emit("event", {
directory: event.location?.directory ?? ctx?.directory, directory: event.location?.directory ?? ctx?.directory,
project: ctx?.project.id, project: ctx?.project.id,
@@ -53,7 +59,7 @@ export const layer = Layer.effect(
id: event.id, id: event.id,
type: EventV2.versionedType(event.type, event.durable.version), type: EventV2.versionedType(event.type, event.durable.version),
seq: event.durable.seq, seq: event.durable.seq,
aggregateID: event.durable.aggregateID, aggregateID,
data: event.data, data: event.data,
}, },
}, },
+9 -2
View File
@@ -1,7 +1,7 @@
import { EventV2 } from "@opencode-ai/core/event"
import { Schema } from "effect" import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { Process } from "@/util/process" import { Process } from "@/util/process"
import { IdeEvent } from "@opencode-ai/schema/ide-event"
const SUPPORTED_IDES = [ const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" }, { name: "Windsurf" as const, cmd: "windsurf" },
@@ -11,7 +11,14 @@ const SUPPORTED_IDES = [
{ name: "VSCodium" as const, cmd: "codium" }, { name: "VSCodium" as const, cmd: "codium" },
] ]
export const Event = IdeEvent export const Event = {
Installed: EventV2.define({
type: "ide.installed",
schema: {
ide: Schema.String,
},
}),
}
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {}) export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {})
+15 -2
View File
@@ -8,17 +8,30 @@ import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import path from "path" import path from "path"
import { EventV2 } from "@opencode-ai/core/event"
import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import semver from "semver" import semver from "semver"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { NpmConfig } from "@opencode-ai/core/npm-config" import { NpmConfig } from "@opencode-ai/core/npm-config"
import { InstallationEvent } from "@opencode-ai/schema/installation-event"
export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown"
export type ReleaseType = "patch" | "minor" | "major" export type ReleaseType = "patch" | "minor" | "major"
export const Event = InstallationEvent export const Event = {
Updated: EventV2.define({
type: "installation.updated",
schema: {
version: Schema.String,
},
}),
UpdateAvailable: EventV2.define({
type: "installation.update-available",
schema: {
version: Schema.String,
},
}),
}
export function getReleaseType(current: string, latest: string): ReleaseType { export function getReleaseType(current: string, latest: string): ReleaseType {
const currMajor = semver.major(current) const currMajor = semver.major(current)
+4 -2
View File
@@ -1,6 +1,7 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util" import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import * as LSPClient from "./client" import * as LSPClient from "./client"
import path from "path" import path from "path"
import { pathToFileURL, fileURLToPath } from "url" import { pathToFileURL, fileURLToPath } from "url"
@@ -13,9 +14,10 @@ import { InstanceState } from "@/effect/instance-state"
import { containsPath } from "@/project/instance-context" import { containsPath } from "@/project/instance-context"
import { NonNegativeInt } from "@opencode-ai/core/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { LspEvent } from "@opencode-ai/schema/lsp-event"
export const Event = LspEvent export const Event = {
Updated: EventV2.define({ type: "lsp.updated", schema: {} }),
}
const Position = Schema.Struct({ const Position = Schema.Struct({
line: NonNegativeInt, line: NonNegativeInt,
-2
View File
@@ -115,8 +115,6 @@ export function fetch<T extends { name: string }>(
export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_")
export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name)
export function prompts(client: Client, timeout?: number) { export function prompts(client: Client, timeout?: number) {
if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) if (!client.getServerCapabilities()?.prompts) return Promise.resolve([])
return paginate( return paginate(
+18 -42
View File
@@ -26,6 +26,7 @@ import { McpOAuthProvider, OAUTH_CALLBACK_PATH } from "./oauth-provider"
import { McpOAuthCallback } from "./oauth-callback" import { McpOAuthCallback } from "./oauth-callback"
import { McpAuth } from "./auth" import { McpAuth } from "./auth"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import { TuiEvent } from "@/server/tui-event" import { TuiEvent } from "@/server/tui-event"
import open from "open" import open from "open"
import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
@@ -34,7 +35,6 @@ import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { McpCatalog } from "./catalog" import { McpCatalog } from "./catalog"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
const DEFAULT_TIMEOUT = 30_000 const DEFAULT_TIMEOUT = 30_000
const CLIENT_OPTIONS = { const CLIENT_OPTIONS = {
@@ -59,9 +59,20 @@ export const Resource = Schema.Struct({
}).annotate({ identifier: "McpResource" }) }).annotate({ identifier: "McpResource" })
export type Resource = Schema.Schema.Type<typeof Resource> export type Resource = Schema.Schema.Type<typeof Resource>
export const ToolsChanged = McpEvent.ToolsChanged export const ToolsChanged = EventV2.define({
type: "mcp.tools.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = McpEvent.BrowserOpenFailed export const BrowserOpenFailed = EventV2.define({
type: "mcp.browser.open.failed",
schema: {
mcpName: Schema.String,
url: Schema.String,
},
})
export const Failed = NamedError.create("MCPFailed", { export const Failed = NamedError.create("MCPFailed", {
name: Schema.String, name: Schema.String,
@@ -129,7 +140,6 @@ interface CreateResult {
mcpClient?: MCPClient mcpClient?: MCPClient
status: Status status: Status
defs?: MCPToolDef[] defs?: MCPToolDef[]
instructions?: string
} }
interface AuthResult { interface AuthResult {
@@ -145,19 +155,11 @@ interface State {
status: Record<string, Status> status: Record<string, Status>
clients: Record<string, MCPClient> clients: Record<string, MCPClient>
defs: Record<string, MCPToolDef[]> defs: Record<string, MCPToolDef[]>
instructions: Record<string, string>
}
export interface ServerInstructions {
name: string
instructions: string
tools: string[]
} }
export interface Interface { export interface Interface {
readonly status: () => Effect.Effect<Record<string, Status>> readonly status: () => Effect.Effect<Record<string, Status>>
readonly clients: () => Effect.Effect<Record<string, MCPClient>> readonly clients: () => Effect.Effect<Record<string, MCPClient>>
readonly instructions: () => Effect.Effect<ServerInstructions[]>
readonly tools: () => Effect.Effect<Record<string, Tool>> readonly tools: () => Effect.Effect<Record<string, Tool>>
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>> readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>> readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
@@ -381,12 +383,7 @@ export const layer = Layer.effect(
if (!listed) { if (!listed) {
return yield* Effect.fail(new Error("Failed to get tools")) return yield* Effect.fail(new Error("Failed to get tools"))
} }
return { return { mcpClient, status, defs: listed } satisfies CreateResult
mcpClient,
status,
defs: listed,
instructions: mcpClient.getInstructions()?.trim(),
} satisfies CreateResult
}).pipe( }).pipe(
Effect.catchCause((cause) => Effect.catchCause((cause) =>
Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore, Effect.andThen(Effect.failCause(cause))), Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore, Effect.andThen(Effect.failCause(cause))),
@@ -433,7 +430,6 @@ export const layer = Layer.effect(
if (s.clients[name] !== client) return if (s.clients[name] !== client) return
delete s.clients[name] delete s.clients[name]
delete s.defs[name] delete s.defs[name]
delete s.instructions[name]
s.status[name] = { status: "failed", error: "Connection closed" } s.status[name] = { status: "failed", error: "Connection closed" }
bridge.fork( bridge.fork(
Effect.logWarning("MCP connection closed", { server: name }).pipe( Effect.logWarning("MCP connection closed", { server: name }).pipe(
@@ -488,7 +484,6 @@ export const layer = Layer.effect(
status: {}, status: {},
clients: {}, clients: {},
defs: {}, defs: {},
instructions: {},
} }
yield* Effect.forEach( yield* Effect.forEach(
@@ -510,7 +505,6 @@ export const layer = Layer.effect(
if (result.mcpClient) { if (result.mcpClient) {
s.clients[key] = result.mcpClient s.clients[key] = result.mcpClient
s.defs[key] = result.defs! s.defs[key] = result.defs!
if (result.instructions) s.instructions[key] = result.instructions
watch(s, key, result.mcpClient, bridge, mcp.timeout) watch(s, key, result.mcpClient, bridge, mcp.timeout)
} }
}), }),
@@ -522,7 +516,6 @@ export const layer = Layer.effect(
const clients = Object.values(s.clients) const clients = Object.values(s.clients)
s.clients = {} s.clients = {}
s.defs = {} s.defs = {}
s.instructions = {}
yield* Effect.forEach( yield* Effect.forEach(
clients, clients,
(client) => (client) =>
@@ -552,7 +545,6 @@ export const layer = Layer.effect(
const client = s.clients[name] const client = s.clients[name]
delete s.clients[name] delete s.clients[name]
delete s.defs[name] delete s.defs[name]
delete s.instructions[name]
if (!client) return Effect.void if (!client) return Effect.void
return Effect.tryPromise(() => client.close()).pipe(Effect.ignore) return Effect.tryPromise(() => client.close()).pipe(Effect.ignore)
} }
@@ -562,7 +554,6 @@ export const layer = Layer.effect(
name: string, name: string,
client: MCPClient, client: MCPClient,
listed: MCPToolDef[], listed: MCPToolDef[],
instructions: string | undefined,
timeout?: number, timeout?: number,
) { ) {
const bridge = yield* EffectBridge.make() const bridge = yield* EffectBridge.make()
@@ -570,8 +561,6 @@ export const layer = Layer.effect(
s.status[name] = { status: "connected" } s.status[name] = { status: "connected" }
s.clients[name] = client s.clients[name] = client
s.defs[name] = listed s.defs[name] = listed
if (instructions) s.instructions[name] = instructions
else delete s.instructions[name]
watch(s, name, client, bridge, timeout) watch(s, name, client, bridge, timeout)
if (previous) yield* Effect.tryPromise(() => previous.close()).pipe(Effect.ignore) if (previous) yield* Effect.tryPromise(() => previous.close()).pipe(Effect.ignore)
return s.status[name] return s.status[name]
@@ -601,18 +590,6 @@ export const layer = Layer.effect(
return s.clients return s.clients
}) })
const instructions = Effect.fn("MCP.instructions")(function* () {
const s = yield* InstanceState.get(state)
return Object.entries(s.instructions)
.filter(([name]) => s.status[name]?.status === "connected")
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, item]) => ({
name,
instructions: item,
tools: (s.defs[name] ?? []).map((tool) => McpCatalog.toolName(name, tool.name)),
}))
})
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) { const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) {
const s = yield* InstanceState.get(state) const s = yield* InstanceState.get(state)
const result = yield* create(name, mcp) const result = yield* create(name, mcp)
@@ -624,7 +601,7 @@ export const layer = Layer.effect(
return result.status return result.status
} }
return yield* storeClient(s, name, result.mcpClient, result.defs!, result.instructions, mcp.timeout) return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout)
}) })
const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCPV1.Info) { const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCPV1.Info) {
@@ -670,7 +647,7 @@ export const layer = Layer.effect(
} }
const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout) const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout)
for (const mcpTool of listed) { for (const mcpTool of listed) {
const key = McpCatalog.toolName(clientName, mcpTool.name) const key = McpCatalog.sanitize(clientName) + "_" + McpCatalog.sanitize(mcpTool.name)
result[key] = McpCatalog.convertTool(mcpTool, client, timeout) result[key] = McpCatalog.convertTool(mcpTool, client, timeout)
} }
} }
@@ -878,7 +855,7 @@ export const layer = Layer.effect(
const s = yield* InstanceState.get(state) const s = yield* InstanceState.get(state)
yield* auth.clearOAuthState(mcpName) yield* auth.clearOAuthState(mcpName)
return yield* storeClient(s, mcpName, client, listed, client.getInstructions()?.trim(), mcpConfig.timeout) return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout)
} }
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
@@ -965,7 +942,6 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
status, status,
clients, clients,
instructions,
tools, tools,
prompts, prompts,
resources, resources,
+35 -40
View File
@@ -1,3 +1,4 @@
import { createConnection } from "net"
import { createServer } from "http" import { createServer } from "http"
import { escapeHtml } from "@/util/html" import { escapeHtml } from "@/util/html"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider" import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
@@ -56,7 +57,6 @@ interface PendingAuth {
} }
let server: ReturnType<typeof createServer> | undefined let server: ReturnType<typeof createServer> | undefined
let starting: Promise<void> | undefined
const pendingAuths = new Map<string, PendingAuth>() const pendingAuths = new Map<string, PendingAuth>()
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can // Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
// find the right entry in pendingAuths (which is keyed by oauthState). // find the right entry in pendingAuths (which is keyed by oauthState).
@@ -144,49 +144,31 @@ function handleRequest(req: import("http").IncomingMessage, res: import("http").
} }
export async function ensureRunning(redirectUri?: string): Promise<void> { export async function ensureRunning(redirectUri?: string): Promise<void> {
if (starting) { // Parse the redirect URI to get port and path (uses defaults if not provided)
await starting const { port, path } = parseRedirectUri(redirectUri)
return ensureRunning(redirectUri)
// If server is running on a different port/path, stop it first
if (server && (currentPort !== port || currentPath !== path)) {
await stop()
} }
const operation = (async () => { if (server) return
// Parse the redirect URI to get port and path (uses defaults if not provided)
const { port, path } = parseRedirectUri(redirectUri)
// If server is running on a different port/path, stop it first const running = await isPortInUse(port)
if (server && (currentPort !== port || currentPath !== path)) await stop() if (running) {
if (server) return return
const candidate = createServer(handleRequest)
try {
await new Promise<void>((resolve, reject) => {
candidate.once("error", reject)
candidate.listen(port, OAUTH_CALLBACK_HOST, resolve)
})
} catch (error) {
candidate.close()
if ((error as NodeJS.ErrnoException).code === "EADDRINUSE") {
throw new Error(
`OAuth callback port ${port} is already in use. ` +
`Set "oauth.callbackPort" (or "oauth.redirectUri") on the MCP server ` +
`entry in your opencode config to use a different port.`,
{ cause: error },
)
}
throw error
}
currentPort = port
currentPath = path
server = candidate
})()
starting = operation
try {
await operation
} finally {
starting = undefined
} }
currentPort = port
currentPath = path
server = createServer(handleRequest)
await new Promise<void>((resolve, reject) => {
server!.listen(currentPort, OAUTH_CALLBACK_HOST, () => {
resolve()
})
server!.on("error", reject)
})
} }
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> { export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
@@ -219,6 +201,19 @@ export function cancelPending(mcpName: string): void {
} }
} }
export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise<boolean> {
return new Promise((resolve) => {
const socket = createConnection(port, "127.0.0.1")
socket.on("connect", () => {
socket.destroy()
resolve(true)
})
socket.on("error", () => {
resolve(false)
})
})
}
export async function stop(): Promise<void> { export async function stop(): Promise<void> {
if (server) { if (server) {
await new Promise<void>((resolve) => server!.close(() => resolve())) await new Promise<void>((resolve) => server!.close(() => resolve()))

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